|
root / src / core / port.reef
port.reef Reef 589 lines 19.1 KB
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
/******************************************************************************
               __               ____                __
              / /   ___  ____ _/ __/_____________ _/ /__
             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/

   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com

   Project: Zygaena
  Filename: port.reef
   Authors: Chris Tusa <chris.tusa@leafscale.com>
   License: <see LICENSE file included with this source code>
Description: Port loading and parsing from package.toml

******************************************************************************/

module core.port

import types
import io.file
import io.dir
import io.path
import core.str
import core.config
import encoding.toml

export
    // Load a port from a directory path, returns PortResult
    fn load(port_path: string): types.PortResult

    // Find a port by name, searches all categories
    fn find(name: string): types.PortResult

    // List all ports in a category
    fn list_category(category: string, names: [string], max_count: int): int

    // Get all categories
    fn get_categories(cats: [string], max_count: int): int
end export

// Dynamically discover categories by scanning the ports directory
// Excludes hidden dirs, infrastructure dirs (pkgs/, groups/), and config-aware paths
fn get_categories(cats: [string], max_count: int): int
    let cfg = config.load()
    let ports_dir = config.get_ports_dir(cfg)

    if not dir.dir_exists(ports_dir)
        return 0
    end if

    let packages_path = config.get_packages_dir(cfg)
    let sources_path = config.get_sources_dir(cfg)
    let build_path = config.get_build_dir(cfg)

    // Extract first path component relative to ports_dir for each configured path
    let skip1 = get_relative_top_dir(ports_dir, packages_path)
    let skip2 = get_relative_top_dir(ports_dir, sources_path)
    let skip3 = get_relative_top_dir(ports_dir, build_path)

    // Also skip the groups directory
    let groups_dir = types.get_groups_dir()
    let skip_groups = get_relative_top_dir(ports_dir, groups_dir)

    mut entries: [string] = new [string](256)
    let entry_count = dir.list_dir(ports_dir, entries, 256)

    mut count = 0
    mut i = 0
    while i < entry_count and count < max_count
        let entry = entries[i]
        let elen = str.length(entry)

        // Skip hidden directories
        if elen == 0 or entry[0] == '.'
            i = i + 1
            continue
        end if

        // Skip infrastructure directories
        if str.equals(entry, skip1) or str.equals(entry, skip2) or str.equals(entry, skip3) or str.equals(entry, skip_groups)
            i = i + 1
            continue
        end if

        // Verify it's actually a directory
        let entry_path = path.join_path(ports_dir, entry)
        if dir.dir_exists(entry_path)
            cats[count] = entry
            count = count + 1
        end if

        i = i + 1
    end while

    return count
end get_categories

// Extract the first path component of `child` relative to `parent`
// e.g. parent="/usr/zports", child="/usr/zports/pkgs/packages" -> "pkgs"
// Returns empty string if child is not under parent
fn get_relative_top_dir(parent: string, child: string): string
    let parent_len = str.length(parent)
    let child_len = str.length(child)

    if child_len <= parent_len
        return ""
    end if

    // Check that child starts with parent
    let prefix = str.substring(child, 0, parent_len)
    if not str.equals(prefix, parent)
        return ""
    end if

    // Skip the separator
    mut start = parent_len
    if child[start] == '/'
        start = start + 1
    end if

    // Find the next slash or end of string
    mut end_pos = start
    while end_pos < child_len
        if child[end_pos] == '/'
            break
        end if
        end_pos = end_pos + 1
    end while

    if end_pos > start
        return str.substring(child, start, end_pos - start)
    end if

    return ""
end get_relative_top_dir

// Parse a TOML array value like ["item1", "item2"] into a string array
// Returns the number of items parsed
fn parse_toml_array(value: string, result: [string], max_items: int): int
    let len = str.length(value)
    if len < 2
        return 0
    end if

    // Check for array brackets
    if value[0] != '['
        // Single value, not an array
        if len > 0
            result[0] = value
            return 1
        end if
        return 0
    end if

    // Skip the opening bracket
    mut pos = 1
    mut count = 0

    while pos < len and count < max_items
        // Skip whitespace
        while pos < len and (value[pos] == ' ' or value[pos] == '\t' or value[pos] == '\n')
            pos = pos + 1
        end while

        if pos >= len or value[pos] == ']'
            break
        end if

        // Check for quoted string
        if value[pos] == '"'
            pos = pos + 1
            mut item_start = pos
            // Find closing quote
            while pos < len and value[pos] != '"'
                pos = pos + 1
            end while
            if pos > item_start
                result[count] = str.substring(value, item_start, pos - item_start)
                count = count + 1
            end if
            pos = pos + 1  // Skip closing quote
        elif value[pos] != ']' and value[pos] != ','
            // Unquoted value
            mut item_start = pos
            while pos < len and value[pos] != ',' and value[pos] != ']' and value[pos] != ' '
                pos = pos + 1
            end while
            if pos > item_start
                result[count] = str.substring(value, item_start, pos - item_start)
                count = count + 1
            end if
        end if

        // Skip comma and whitespace
        while pos < len and (value[pos] == ',' or value[pos] == ' ' or value[pos] == '\t' or value[pos] == '\n')
            pos = pos + 1
        end while
    end while

    return count
end parse_toml_array

// Create an empty/default Port structure (delegate to types module)
fn new_port(): types.Port
    return types.new_port()
end new_port

// Helper to create error result
fn error_result(msg: string): types.PortResult
    return types.new_port_result(false, new_port(), msg)
end error_result

// Helper to create success result
fn success_result(p: types.Port): types.PortResult
    return types.new_port_result(true, p, "")
end success_result

// Extract filename from URL
fn extract_filename_from_url(url: string): string
    let len = str.length(url)
    if len == 0
        return ""
    end if

    // Find last slash
    mut last_slash = -1
    mut i = len - 1
    while i >= 0
        if url[i] == '/'
            last_slash = i
            break
        end if
        i = i - 1
    end while

    if last_slash < 0
        return url
    end if

    let filename = str.substring(url, last_slash + 1, len - last_slash - 1)

    // Remove query string if present
    let query_len = str.length(filename)
    mut query_pos = -1
    i = 0
    while i < query_len
        if filename[i] == '?'
            query_pos = i
            break
        end if
        i = i + 1
    end while

    if query_pos > 0
        return str.substring(filename, 0, query_pos)
    end if

    return filename
end extract_filename_from_url

// Parse sources from TOML - supports both new [[source]] and legacy [sources] format
fn parse_sources(keys: [string], vals: [string], entry_count: int): types.SourceInfo
    mut info = types.new_source_info()

    // Try new [[source]] format first (source.0.file, source.1.file, etc.)
    mut source_idx = 0
    mut found_new_format = false

    while source_idx < 16
        let prefix = "source." + int_to_str(source_idx)
        let file_key = prefix + ".file"

        if not toml.toml_has_key(keys, vals, entry_count, file_key)
            break
        end if

        found_new_format = true

        mut src = types.new_source()

        // Get file name
        src.file = toml.toml_get(keys, vals, entry_count, file_key)

        // Get URLs array
        let urls_key = prefix + ".urls"
        if toml.toml_has_key(keys, vals, entry_count, urls_key)
            let urls_val = toml.toml_get(keys, vals, entry_count, urls_key)
            mut urls_arr: [string] = new [string](16)
            let urls_count = parse_toml_array(urls_val, urls_arr, 16)
            src.urls = urls_arr
            src.url_count = urls_count
        end if

        // Get checksum (single value)
        let checksum_key = prefix + ".checksum"
        if toml.toml_has_key(keys, vals, entry_count, checksum_key)
            src.checksum = toml.toml_get(keys, vals, entry_count, checksum_key)
        end if

        // Get extract flag (default: true)
        let extract_key = prefix + ".extract"
        if toml.toml_has_key(keys, vals, entry_count, extract_key)
            src.extract = toml.toml_get_bool(keys, vals, entry_count, extract_key)
        end if

        info.sources[source_idx] = src
        source_idx = source_idx + 1
    end while

    if found_new_format
        info.count = source_idx
        return info
    end if

    // Fall back to legacy [sources] format
    if toml.toml_has_key(keys, vals, entry_count, "sources.urls")
        let urls_val = toml.toml_get(keys, vals, entry_count, "sources.urls")
        mut urls_arr: [string] = new [string](16)
        let urls_count = parse_toml_array(urls_val, urls_arr, 16)

        // Parse checksums
        mut checksums_arr: [string] = new [string](16)
        if toml.toml_has_key(keys, vals, entry_count, "sources.checksums")
            let cksum_val = toml.toml_get(keys, vals, entry_count, "sources.checksums")
            parse_toml_array(cksum_val, checksums_arr, 16)
        end if

        // Convert legacy format to new Source structs
        mut i = 0
        while i < urls_count
            mut src = types.new_source()
            src.file = extract_filename_from_url(urls_arr[i])
            src.urls[0] = urls_arr[i]
            src.url_count = 1
            if str.length(checksums_arr[i]) > 0
                src.checksum = checksums_arr[i]
            end if
            src.extract = true
            info.sources[i] = src
            i = i + 1
        end while
        info.count = urls_count
    end if

    return info
end parse_sources

// Helper: convert int to string (for source index keys)
fn int_to_str(n: int): string
    if n == 0
        return "0"
    end if

    mut result = ""
    mut value = n
    while value > 0
        let digit = value % 10
        result = str.concat(str.substring("0123456789", digit, 1), result)
        value = value / 10
    end while

    return result
end int_to_str

// Load a port from a directory containing package.toml
fn load(port_path: string): types.PortResult
    // Check if directory exists
    if not dir.dir_exists(port_path)
        return error_result("Port directory does not exist: " + port_path)
    end if

    // Check for package.toml
    let toml_path = path.join_path(port_path, "package.toml")
    if not file.fileExists(toml_path)
        return error_result("package.toml not found in: " + port_path)
    end if

    // Read the TOML file
    let content = file.readFile(toml_path)
    if str.length(content) == 0
        return error_result("Failed to read package.toml or file is empty")
    end if

    // Parse TOML
    let keys = toml.toml_alloc_keys()
    let vals = toml.toml_alloc_values()
    let entry_count = toml.toml_parse(content, keys, vals)

    if entry_count == 0
        return error_result("Failed to parse package.toml")
    end if

    // Build the Port structure
    mut port = new_port()
    port.port_dir = port_path

    // Extract category from path (e.g., /usr/zports/base/vim -> base)
    let parent_dir = path.dirname(port_path)
    port.category = path.basename(parent_dir)

    // Parse [package] section
    port.info.name = toml.toml_get(keys, vals, entry_count, "package.name")
    port.info.version = toml.toml_get(keys, vals, entry_count, "package.version")
    port.info.release = toml.toml_get_int(keys, vals, entry_count, "package.release")
    port.info.description = toml.toml_get(keys, vals, entry_count, "package.description")
    port.info.url = toml.toml_get(keys, vals, entry_count, "package.url")
    port.info.license = toml.toml_get(keys, vals, entry_count, "package.license")
    port.info.maintainer = toml.toml_get(keys, vals, entry_count, "package.maintainer")

    // Architecture (default to x86_64 if not specified)
    let arch_val = toml.toml_get(keys, vals, entry_count, "package.arch")
    if str.length(arch_val) > 0
        port.info.arch = arch_val
    end if

    // Parse alternatives array
    if toml.toml_has_key(keys, vals, entry_count, "package.alternatives")
        let alt_val = toml.toml_get(keys, vals, entry_count, "package.alternatives")
        mut alt_arr: [string] = new [string](32)
        let alt_count = parse_toml_array(alt_val, alt_arr, 32)
        port.info.alternatives = alt_arr
        port.info.alternatives_count = alt_count
    end if

    // Parse conflicts array
    if toml.toml_has_key(keys, vals, entry_count, "package.conflicts")
        let conf_val = toml.toml_get(keys, vals, entry_count, "package.conflicts")
        mut conf_arr: [string] = new [string](32)
        let conf_count = parse_toml_array(conf_val, conf_arr, 32)
        port.info.conflicts = conf_arr
        port.info.conflicts_count = conf_count
    end if

    // Parse [dependencies] section
    if toml.toml_has_key(keys, vals, entry_count, "dependencies.runtime")
        let rt_val = toml.toml_get(keys, vals, entry_count, "dependencies.runtime")
        mut rt_arr: [string] = new [string](64)
        let rt_count = parse_toml_array(rt_val, rt_arr, 64)
        port.deps.runtime = rt_arr
        port.deps.runtime_count = rt_count
    end if

    if toml.toml_has_key(keys, vals, entry_count, "dependencies.build")
        let bd_val = toml.toml_get(keys, vals, entry_count, "dependencies.build")
        mut bd_arr: [string] = new [string](64)
        let bd_count = parse_toml_array(bd_val, bd_arr, 64)
        port.deps.build = bd_arr
        port.deps.build_count = bd_count
    end if

    // Parse sources - try new [[source]] format first, fall back to legacy [sources]
    port.sources = parse_sources(keys, vals, entry_count)

    // Parse [patches] section
    if toml.toml_has_key(keys, vals, entry_count, "patches.files")
        let patch_val = toml.toml_get(keys, vals, entry_count, "patches.files")
        mut patch_arr: [string] = new [string](32)
        let patch_count = parse_toml_array(patch_val, patch_arr, 32)
        port.patches.files = patch_arr
        port.patches.count = patch_count
    end if

    if toml.toml_has_key(keys, vals, entry_count, "patches.strip")
        port.patches.strip = toml.toml_get_int(keys, vals, entry_count, "patches.strip")
    end if

    // Parse [config] section
    if toml.toml_has_key(keys, vals, entry_count, "config.files")
        let cfg_val = toml.toml_get(keys, vals, entry_count, "config.files")
        mut cfg_arr: [string] = new [string](32)
        let cfg_count = parse_toml_array(cfg_val, cfg_arr, 32)
        port.config.files = cfg_arr
        port.config.count = cfg_count
    end if

    // Parse [scripts] section
    if toml.toml_has_key(keys, vals, entry_count, "scripts.pre_install")
        port.scripts.pre_install = toml.toml_get(keys, vals, entry_count, "scripts.pre_install")
    end if
    if toml.toml_has_key(keys, vals, entry_count, "scripts.post_install")
        port.scripts.post_install = toml.toml_get(keys, vals, entry_count, "scripts.post_install")
    end if
    if toml.toml_has_key(keys, vals, entry_count, "scripts.pre_remove")
        port.scripts.pre_remove = toml.toml_get(keys, vals, entry_count, "scripts.pre_remove")
    end if
    if toml.toml_has_key(keys, vals, entry_count, "scripts.post_remove")
        port.scripts.post_remove = toml.toml_get(keys, vals, entry_count, "scripts.post_remove")
    end if

    // Parse [paths] section (per-package overrides)
    if toml.toml_has_key(keys, vals, entry_count, "paths.prefix")
        port.prefix = toml.toml_get(keys, vals, entry_count, "paths.prefix")
    end if
    if toml.toml_has_key(keys, vals, entry_count, "paths.sysconfdir")
        port.sysconfdir = toml.toml_get(keys, vals, entry_count, "paths.sysconfdir")
    end if

    // Parse [build] section
    if toml.toml_has_key(keys, vals, entry_count, "build.parallel")
        port.build_cfg.parallel = toml.toml_get_bool(keys, vals, entry_count, "build.parallel")
    end if
    if toml.toml_has_key(keys, vals, entry_count, "build.jobs")
        port.build_cfg.jobs = toml.toml_get_int(keys, vals, entry_count, "build.jobs")
    end if

    // Validate required fields
    if str.length(port.info.name) == 0
        return types.new_port_result(false, port, "package.name is required")
    end if

    if str.length(port.info.version) == 0
        return types.new_port_result(false, port, "package.version is required")
    end if

    return success_result(port)
end load

// Find a port by name, searching all categories
// Accepts both "kernel" (searches all categories) and "hammerhead/kernel" (direct category/name)
fn find(name: string): types.PortResult
    let cfg = config.load()
    let ports_dir = config.get_ports_dir(cfg)

    // Check if ports directory exists
    if not dir.dir_exists(ports_dir)
        return error_result("Ports directory not found: " + ports_dir + " (check /etc/coral/coral.conf)")
    end if

    // Check if name contains a slash (category/name format)
    let slash_idx = str.index_of(name, "/")
    if slash_idx >= 0
        let port_path = path.join_path(ports_dir, name)
        if dir.dir_exists(port_path)
            return load(port_path)
        end if
        return error_result("Port not found: " + name + " (searched " + ports_dir + "/" + name + ")")
    end if

    // Search all categories for the name
    mut cats: [string] = new [string](32)
    let cat_count = get_categories(cats, 32)

    mut i = 0
    while i < cat_count
        let port_path = path.join_path(ports_dir, path.join_path(cats[i], name))
        if dir.dir_exists(port_path)
            return load(port_path)
        end if
        i = i + 1
    end while

    return error_result("Port not found: " + name + " (searched " + ports_dir + ")")
end find

// List all ports in a given category
fn list_category(category: string, names: [string], max_count: int): int
    let cfg = config.load()
    let cat_path = path.join_path(config.get_ports_dir(cfg), category)

    if not dir.dir_exists(cat_path)
        return 0
    end if

    mut entries: [string] = new [string](256)
    let entry_count = dir.list_dir(cat_path, entries, 256)

    mut count = 0
    mut i = 0
    while i < entry_count and count < max_count
        let entry = entries[i]
        // Skip hidden files and non-directories
        if str.length(entry) > 0 and entry[0] != '.'
            let entry_path = path.join_path(cat_path, entry)
            // Check if it's a valid port (has package.toml)
            let toml_path = path.join_path(entry_path, "package.toml")
            if file.fileExists(toml_path)
                names[count] = entry
                count = count + 1
            end if
        end if
        i = i + 1
    end while

    return count
end list_category

end module