diff --git a/src/pipeline/pass_definitions.c b/src/pipeline/pass_definitions.c index ac147ae5..a9357cfd 100644 --- a/src/pipeline/pass_definitions.c +++ b/src/pipeline/pass_definitions.c @@ -333,6 +333,21 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t int total_imports = 0; int errors = 0; + /* Sequential pass must extract all defs (which create Module/Function/... + * nodes) BEFORE resolving imports — otherwise a workspace import in the + * first file processed can't find the target Module node, because the + * target file's defs haven't been extracted yet. Result cache is + * required for this two-phase ordering. */ + CBMFileResult **local_cache = ctx->result_cache; + bool owns_local_cache = false; + if (!local_cache) { + local_cache = (CBMFileResult **)calloc((size_t)file_count, sizeof(CBMFileResult *)); + owns_local_cache = (local_cache != NULL); + } + + /* Phase 1: extract every file and create def-derived nodes (Modules, + * Functions, ...) so any file's IMPORTS can resolve against the + * complete in-memory graph in Phase 2. */ for (int i = 0; i < file_count; i++) { if (cbm_pipeline_check_cancel(ctx)) { return CBM_NOT_FOUND; @@ -371,17 +386,45 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t /* Store calls for pass_calls (we save them in the extraction results * for now — a future optimization would batch these) */ total_calls += result->calls.count; - total_imports += create_import_edges_for_file(ctx, result, rel); - create_channel_edges_for_file(ctx, result, rel); - /* Cache or free the extraction result */ - if (ctx->result_cache) { - ctx->result_cache[i] = result; + if (local_cache) { + local_cache[i] = result; } else { + /* Cache unavailable: imports for this file can still only + * resolve to defs already in the graph, but the file's + * own defs are now persisted before the lookup. */ + total_imports += create_import_edges_for_file(ctx, result, rel); + create_channel_edges_for_file(ctx, result, rel); cbm_free_result(result); } } + /* Phase 2: now that all extraction results are cached and Module + * nodes for every file are in the graph, walk the cache again to + * create IMPORTS / channel edges. Imports resolve against the full + * project graph. */ + if (local_cache) { + for (int i = 0; i < file_count; i++) { + if (cbm_pipeline_check_cancel(ctx)) { + break; + } + CBMFileResult *result = local_cache[i]; + if (!result) { + continue; + } + total_imports += create_import_edges_for_file(ctx, result, files[i].rel_path); + create_channel_edges_for_file(ctx, result, files[i].rel_path); + } + if (owns_local_cache) { + for (int i = 0; i < file_count; i++) { + if (local_cache[i]) { + cbm_free_result(local_cache[i]); + } + } + free(local_cache); + } + } + cbm_log_info("pass.done", "pass", "definitions", "defs", itoa_log(total_defs), "calls", itoa_log(total_calls), "imports", itoa_log(total_imports), "errors", itoa_log(errors)); diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 8af49f7c..1bcd1cf3 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -554,6 +554,11 @@ static void merge_pkg_entries(cbm_pipeline_ctx_t *ctx, cbm_pkg_entries_t *pkg_en if (!pkg_entries) { return; } + /* Supplement with a repo-wide filesystem walk so manifests filtered + * by the main discoverer (package.json, composer.json — in + * IGNORED_JSON_FILES) still feed pkgmap. Append into worker 0's + * array so the existing merge below sees them. */ + cbm_pkgmap_scan_repo(ctx->repo_path, &pkg_entries[0]); cbm_pipeline_set_pkgmap(cbm_pkgmap_build(pkg_entries, worker_count, ctx->project_name)); for (int i = 0; i < worker_count; i++) { cbm_pkg_entries_free(&pkg_entries[i]); diff --git a/src/pipeline/pass_pkgmap.c b/src/pipeline/pass_pkgmap.c index 007c0ce8..12befff6 100644 --- a/src/pipeline/pass_pkgmap.c +++ b/src/pipeline/pass_pkgmap.c @@ -12,6 +12,7 @@ */ #include "pipeline/pipeline.h" #include "pipeline/pipeline_internal.h" +#include "discover/discover.h" #include "foundation/compat.h" #include "foundation/constants.h" #include "foundation/hash_table.h" @@ -22,10 +23,12 @@ #include +#include #include #include #include #include +#include /* Read an entire file into a malloc'd buffer. Returns NULL on failure. */ static char *pkgmap_read_file(const char *path, int *out_len) { @@ -747,6 +750,100 @@ CBMHashTable *cbm_pkgmap_build(cbm_pkg_entries_t *worker_entries, int worker_cou return map; } +/* Returns true if basename is a package manifest we know how to parse. + * Used by the filesystem walker; cbm_pkgmap_try_parse is the source of + * truth for which basenames produce entries. */ +static bool is_pkgmap_manifest_basename(const char *basename) { + if (!basename) { + return false; + } + if (strcmp(basename, "package.json") == 0 || strcmp(basename, "go.mod") == 0 || + strcmp(basename, "Cargo.toml") == 0 || strcmp(basename, "pyproject.toml") == 0 || + strcmp(basename, "composer.json") == 0 || strcmp(basename, "pubspec.yaml") == 0 || + strcmp(basename, "pom.xml") == 0 || strcmp(basename, "build.gradle") == 0 || + strcmp(basename, "build.gradle.kts") == 0 || strcmp(basename, "mix.exs") == 0) { + return true; + } + return ends_with(basename, ".gemspec"); +} + +/* Recursive filesystem walker that finds and parses package manifest + * files independently of the main discovery filter. The main discovery + * filter intentionally hides package.json / composer.json etc. from + * code indexing (they're config, not source), but pass_pkgmap still + * needs to read them to resolve workspace imports. Skips directories + * matched by the shared cbm_should_skip_dir helper so we don't walk + * node_modules, .git, build, etc. Returns the number of manifests + * parsed, accumulated across the whole walk. */ +static int pkgmap_walk_dir(const char *abs_dir, const char *rel_dir, + cbm_pkg_entries_t *entries) { + DIR *dir = opendir(abs_dir); + if (!dir) { + return 0; + } + int parsed = 0; + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) { + const char *name = entry->d_name; + if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) { + continue; + } + char abs_path[PKGMAP_PATH_BUF]; + char rel_path[PKGMAP_PATH_BUF]; + snprintf(abs_path, sizeof(abs_path), "%s/%s", abs_dir, name); + if (rel_dir && rel_dir[0]) { + snprintf(rel_path, sizeof(rel_path), "%s/%s", rel_dir, name); + } else { + snprintf(rel_path, sizeof(rel_path), "%s", name); + } + struct stat st; + if (lstat(abs_path, &st) != 0) { + continue; + } + if (S_ISLNK(st.st_mode)) { + continue; + } + if (S_ISDIR(st.st_mode)) { + if (cbm_should_skip_dir(name, CBM_MODE_FULL)) { + continue; + } + parsed += pkgmap_walk_dir(abs_path, rel_path, entries); + continue; + } + if (!S_ISREG(st.st_mode)) { + continue; + } + if (!is_pkgmap_manifest_basename(name)) { + continue; + } + int source_len = 0; + char *source = pkgmap_read_file(abs_path, &source_len); + if (!source) { + continue; + } + if (cbm_pkgmap_try_parse(name, rel_path, source, source_len, entries)) { + parsed++; + } + free(source); + } + closedir(dir); + return parsed; +} + +/* Scan a repository for package manifest files via the filesystem + * walker above. Always-available companion to the parallel path's + * per-worker manifest parsing, which is bound to whatever `files[]` + * the discoverer produces and therefore misses ignored manifests like + * package.json. NULL-safe; returns 0 entries when repo_path is unset. */ +int cbm_pkgmap_scan_repo(const char *repo_path, cbm_pkg_entries_t *entries) { + if (!repo_path || !entries) { + return 0; + } + int parsed = pkgmap_walk_dir(repo_path, "", entries); + cbm_log_info("pkgmap.scan_repo", "manifests", pkgmap_itoa(parsed)); + return parsed; +} + /* Build pkgmap for sequential path (reads manifest files directly) */ CBMHashTable *cbm_pkgmap_build_from_files(const cbm_file_info_t *files, int file_count, const char *project_name) { @@ -755,15 +852,7 @@ CBMHashTable *cbm_pkgmap_build_from_files(const cbm_file_info_t *files, int file for (int i = 0; i < file_count; i++) { const char *basename = path_basename(files[i].rel_path); - /* Quick check: is this a manifest file? */ - bool is_manifest = - (strcmp(basename, "package.json") == 0 || strcmp(basename, "go.mod") == 0 || - strcmp(basename, "Cargo.toml") == 0 || strcmp(basename, "pyproject.toml") == 0 || - strcmp(basename, "composer.json") == 0 || strcmp(basename, "pubspec.yaml") == 0 || - strcmp(basename, "pom.xml") == 0 || strcmp(basename, "build.gradle") == 0 || - strcmp(basename, "build.gradle.kts") == 0 || strcmp(basename, "mix.exs") == 0 || - ends_with(basename, ".gemspec")); - if (!is_manifest) { + if (!is_pkgmap_manifest_basename(basename)) { continue; } @@ -782,6 +871,44 @@ CBMHashTable *cbm_pkgmap_build_from_files(const cbm_file_info_t *files, int file return map; } +/* Variant of cbm_pkgmap_build_from_files that ALSO walks the repo + * filesystem to pick up manifests filtered out by the main discoverer + * (the canonical case: package.json, which is in IGNORED_JSON_FILES). + * Falls back to the files[]-only behaviour if repo_path is NULL. */ +CBMHashTable *cbm_pkgmap_build_from_repo(const char *repo_path, const cbm_file_info_t *files, + int file_count, const char *project_name) { + cbm_pkg_entries_t entries; + cbm_pkg_entries_init(&entries); + + /* Manifests already visible through discovery (Cargo.toml, go.mod, + * pyproject.toml, ...). package.json typically isn't, but we still + * harvest whatever the discovery filter exposed in case downstream + * filters change. */ + int from_files = 0; + for (int i = 0; i < file_count; i++) { + const char *basename = path_basename(files[i].rel_path); + if (!is_pkgmap_manifest_basename(basename)) { + continue; + } + from_files++; + int source_len = 0; + char *source = pkgmap_read_file(files[i].path, &source_len); + if (!source) { + continue; + } + cbm_pkgmap_try_parse(basename, files[i].rel_path, source, source_len, &entries); + free(source); + } + + int from_walk = cbm_pkgmap_scan_repo(repo_path, &entries); + cbm_log_info("pkgmap.scan", "manifests_from_files", pkgmap_itoa(from_files), + "manifests_from_walk", pkgmap_itoa(from_walk), + "entries", pkgmap_itoa(entries.count)); + CBMHashTable *map = cbm_pkgmap_build(&entries, SKIP_ONE, project_name); + cbm_pkg_entries_free(&entries); + return map; +} + static void pkgmap_free_entry(const char *key, void *value, void *userdata) { (void)userdata; free((void *)key); diff --git a/src/pipeline/pass_route_nodes.c b/src/pipeline/pass_route_nodes.c index 0747447f..b436a279 100644 --- a/src/pipeline/pass_route_nodes.c +++ b/src/pipeline/pass_route_nodes.c @@ -779,6 +779,281 @@ static void create_data_flows(cbm_gbuf_t *gb) { } } +/* ── SvelteKit filesystem-based route extractor ────────────────────── + * + * SvelteKit derives REST endpoints and SSR loaders from the filesystem + * layout under `src/routes/`. No `app.get(...)` style call exists for + * pass_calls.c to pick up, so we walk File nodes and synthesise Route + * nodes + HANDLES edges directly here. + * + * Recognised filenames (TypeScript and JavaScript variants): + * + * +server.{ts,js} → REST endpoints, exports GET/POST/... + * +page.server.{ts,js} → SSR page server module: load + actions + * +layout.server.{ts,js} → SSR layout server module: load + * + * Route path is derived from the directory chain under `routes/`: + * + * apps/x/src/routes/foo/+server.ts → /foo + * apps/x/src/routes/api/items/+server.ts → /api/items + * apps/x/src/routes/(grp)/foo/+server.ts → /foo (group stripped) + * apps/x/src/routes/[slug]/+server.ts → /:slug (param rewrite) + * apps/x/src/routes/+server.ts → / (root) + */ + +enum { + SKR_PATH_BUF = 1024, + SKR_NAME_BUF = 64, +}; + +/* Detect the SvelteKit kind of a file path. Returns 1 for +server, + * 2 for +page.server, 3 for +layout.server; 0 if not a SvelteKit + * server-side route file. */ +static int sveltekit_file_kind(const char *file_path) { + if (!file_path) { + return 0; + } + /* The basename must match one of the three patterns. We also require + * a "/routes/" segment somewhere in the path so we don't snag files + * in unrelated directories that happen to be named "+server.ts". */ + if (!strstr(file_path, "/routes/")) { + return 0; + } + const char *slash = strrchr(file_path, '/'); + const char *base = slash ? slash + 1 : file_path; + if (strcmp(base, "+server.ts") == 0 || strcmp(base, "+server.js") == 0) { + return 1; + } + if (strcmp(base, "+page.server.ts") == 0 || strcmp(base, "+page.server.js") == 0) { + return 2; + } + if (strcmp(base, "+layout.server.ts") == 0 || strcmp(base, "+layout.server.js") == 0) { + return 3; + } + return 0; +} + +/* Compute the URL route path for a SvelteKit file path. Writes into + * `out` (caller-provided buffer) and returns the buffer or NULL on + * error. The leading "/" is always present; "/" is used for the root + * route. Group segments wrapped in parentheses are stripped. Dynamic + * params `[slug]` become `:slug`, and rest params `[...slug]` become + * `*slug` so the result is recognisable as a path pattern. */ +static const char *sveltekit_route_path(const char *file_path, char *out, int outsz) { + if (!file_path || !out || outsz <= 1) { + return NULL; + } + const char *routes_seg = strstr(file_path, "/routes/"); + if (!routes_seg) { + return NULL; + } + /* Walk segment-by-segment between "/routes/" and the trailing "+...". */ + const char *p = routes_seg + strlen("/routes/"); + const char *last_slash = strrchr(file_path, '/'); + if (!last_slash || last_slash < p) { + /* File is directly under /routes/ — root route. */ + out[0] = '/'; + out[1] = '\0'; + return out; + } + /* p points at first char after /routes/, last_slash points at the + * slash before the +...filename. Iterate segments between them. */ + int pos = 0; + out[pos] = '\0'; + while (p < last_slash) { + const char *seg_end = strchr(p, '/'); + if (!seg_end || seg_end > last_slash) { + seg_end = last_slash; + } + size_t seg_len = (size_t)(seg_end - p); + if (seg_len > 0) { + /* Group `(name)` — skip entirely. */ + if (p[0] == '(' && p[seg_len - 1] == ')') { + p = seg_end + 1; + continue; + } + /* Emit "/" then rewritten segment. */ + if (pos + 1 < outsz - 1) { + out[pos++] = '/'; + } + /* Dynamic param: `[slug]` → `:slug`, `[...slug]` → `*slug`. */ + if (seg_len >= 2 && p[0] == '[' && p[seg_len - 1] == ']') { + const char *inner = p + 1; + size_t inner_len = seg_len - 2; + if (inner_len >= 3 && strncmp(inner, "...", 3) == 0) { + if (pos < outsz - 1) { + out[pos++] = '*'; + } + inner += 3; + inner_len -= 3; + } else { + if (pos < outsz - 1) { + out[pos++] = ':'; + } + } + /* Strip an optional `=matcher` suffix after the param name. */ + size_t copy_len = inner_len; + for (size_t i = 0; i < inner_len; i++) { + if (inner[i] == '=') { + copy_len = i; + break; + } + } + if ((int)copy_len > outsz - 1 - pos) { + copy_len = (size_t)(outsz - 1 - pos); + } + memcpy(out + pos, inner, copy_len); + pos += (int)copy_len; + } else { + size_t copy_len = seg_len; + if ((int)copy_len > outsz - 1 - pos) { + copy_len = (size_t)(outsz - 1 - pos); + } + memcpy(out + pos, p, copy_len); + pos += (int)copy_len; + } + out[pos] = '\0'; + } + p = seg_end + 1; + } + if (pos == 0) { + out[pos++] = '/'; + out[pos] = '\0'; + } + return out; +} + +/* Returns the HTTP method string a function name maps to for a +server + * file, or NULL if the function isn't a SvelteKit REST handler. The set + * mirrors SvelteKit's documented HTTP verb exports. */ +static const char *sveltekit_server_method(const char *name) { + if (!name) { + return NULL; + } + static const char *const verbs[] = { + "GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD", + }; + for (size_t i = 0; i < sizeof(verbs) / sizeof(verbs[0]); i++) { + if (strcmp(name, verbs[i]) == 0) { + return verbs[i]; + } + } + /* `fallback` catches any verb not explicitly exported. */ + if (strcmp(name, "fallback") == 0) { + return "ANY"; + } + return NULL; +} + +typedef struct { + cbm_gbuf_t *gb; + int routes_created; + int handles_created; + int files_seen; +} sveltekit_ctx_t; + +/* Process one File node: if it matches a SvelteKit server-side file, + * synthesise a Route node and HANDLES edges from any handler functions + * (or actions Variable) defined in the file. */ +static void sveltekit_file_visitor(const cbm_gbuf_node_t *node, void *userdata) { + sveltekit_ctx_t *ctx = (sveltekit_ctx_t *)userdata; + if (!node || !node->label || strcmp(node->label, "File") != 0) { + return; + } + int kind = sveltekit_file_kind(node->file_path); + if (kind == 0) { + return; + } + ctx->files_seen++; + + char route_path[SKR_PATH_BUF]; + if (!sveltekit_route_path(node->file_path, route_path, sizeof(route_path))) { + return; + } + + /* Find every DEFINES edge from this file to a Function or Variable. */ + const cbm_gbuf_edge_t **edges = NULL; + int edge_count = 0; + if (cbm_gbuf_find_edges_by_source_type(ctx->gb, node->id, "DEFINES", &edges, &edge_count) != 0 + || edge_count == 0) { + return; + } + + for (int i = 0; i < edge_count; i++) { + const cbm_gbuf_node_t *child = cbm_gbuf_find_by_id(ctx->gb, edges[i]->target_id); + if (!child || !child->name || !child->label) { + continue; + } + bool is_fn = strcmp(child->label, "Function") == 0; + bool is_var = strcmp(child->label, "Variable") == 0; + if (!is_fn && !is_var) { + continue; + } + + const char *method = NULL; + bool is_actions = false; + if (kind == 1) { + method = sveltekit_server_method(child->name); + } else if (kind == 2) { + if (is_fn && strcmp(child->name, "load") == 0) { + method = "LOAD"; + } else if (strcmp(child->name, "actions") == 0) { + /* SvelteKit form actions: emit a single Route at this + * path with method=ACTIONS; finer-grained action names + * aren't exposed as top-level identifiers. */ + method = "ACTIONS"; + is_actions = is_var; + } + } else if (kind == 3) { + if (is_fn && strcmp(child->name, "load") == 0) { + method = "LOAD"; + } + } + if (!method) { + continue; + } + + char route_qn[CBM_ROUTE_QN_SIZE]; + snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", method, route_path); + char route_props[CBM_SZ_256]; + snprintf(route_props, sizeof(route_props), + "{\"method\":\"%s\",\"framework\":\"sveltekit\"}", method); + int64_t route_id = cbm_gbuf_upsert_node(ctx->gb, "Route", route_path, route_qn, "", 0, 0, + route_props); + if (route_id == 0) { + continue; + } + ctx->routes_created++; + + char hprops[CBM_SZ_256]; + snprintf(hprops, sizeof(hprops), + "{\"handler\":\"%s\",\"framework\":\"sveltekit\"%s}", + child->qualified_name ? child->qualified_name : child->name, + is_actions ? ",\"via\":\"actions_object\"" : ""); + cbm_gbuf_insert_edge(ctx->gb, child->id, route_id, "HANDLES", hprops); + ctx->handles_created++; + } +} + +/* Public entry point: scan all File nodes for SvelteKit server modules + * and synthesise Route + HANDLES nodes from the filesystem convention. */ +static void create_sveltekit_routes(cbm_gbuf_t *gb) { + if (!gb) { + return; + } + sveltekit_ctx_t ctx = {.gb = gb, .routes_created = 0, .handles_created = 0, .files_seen = 0}; + cbm_gbuf_foreach_node(gb, sveltekit_file_visitor, &ctx); + if (ctx.files_seen > 0) { + char b1[CBM_SZ_16]; + char b2[CBM_SZ_16]; + char b3[CBM_SZ_16]; + snprintf(b1, sizeof(b1), "%d", ctx.files_seen); + snprintf(b2, sizeof(b2), "%d", ctx.routes_created); + snprintf(b3, sizeof(b3), "%d", ctx.handles_created); + cbm_log_info("pass.sveltekit_routes", "files", b1, "routes", b2, "handles", b3); + } +} + void cbm_pipeline_create_route_nodes(cbm_gbuf_t *gb) { if (!gb) { return; @@ -812,4 +1087,9 @@ void cbm_pipeline_create_route_nodes(cbm_gbuf_t *gb) { * Scans Class nodes from .proto files, follows DEFINES_METHOD edges * to find rpc methods, creates __grpc__ServiceName/MethodName Route nodes. */ create_grpc_routes(gb); + + /* Phase 5: filesystem-based SvelteKit routes (+server / +page.server / + * +layout.server) — no call-site equivalent for pass_calls.c to pick + * up, so we walk File nodes directly here. */ + create_sveltekit_routes(gb); } diff --git a/src/pipeline/pipeline.c b/src/pipeline/pipeline.c index 396e59bf..194af6c2 100644 --- a/src/pipeline/pipeline.c +++ b/src/pipeline/pipeline.c @@ -491,9 +491,12 @@ static int run_sequential_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx, struct timespec *t) { cbm_log_info("pipeline.mode", "mode", "sequential", "files", itoa_buf(file_count)); - /* Build package map from manifest files (sequential: read manifests directly) */ - /* Build package map from manifest files (sequential: read manifests directly) */ - cbm_pipeline_set_pkgmap(cbm_pkgmap_build_from_files(files, file_count, ctx->project_name)); + /* Build package map from manifest files (sequential: read manifests directly). + * Use the repo-walking variant so manifests filtered out by the main + * discoverer (package.json, composer.json) still feed pkgmap and let + * workspace imports like `@my/pkg` resolve to their target Module. */ + cbm_pipeline_set_pkgmap( + cbm_pkgmap_build_from_repo(ctx->repo_path, files, file_count, ctx->project_name)); CBMFileResult **seq_cache = (CBMFileResult **)calloc(file_count, sizeof(CBMFileResult *)); if (seq_cache) { @@ -574,9 +577,16 @@ static int run_parallel_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx, } /* Cross-file LSP: augments per-file resolved_calls with cross-file * type-aware resolutions before parallel_resolve emits CALLS edges. - * Soft-failures only — log and continue. */ + * Soft-failures only — log and continue. + * NOTE: CBM_DISABLE_LSP_CROSS=1 env opts out of the cross-file LSP + * (which can SIGSEGV on large TS projects — separate upstream bug, + * unrelated to pkgmap / SvelteKit fixes). */ cbm_clock_gettime(CLOCK_MONOTONIC, t); - (void)cbm_pipeline_pass_lsp_cross(ctx, files, file_count, cache); + if (getenv("CBM_DISABLE_LSP_CROSS") == NULL) { + (void)cbm_pipeline_pass_lsp_cross(ctx, files, file_count, cache); + } else { + cbm_log_info("lsp_cross.skipped", "reason", "CBM_DISABLE_LSP_CROSS env set"); + } cbm_log_info("pass.timing", "pass", "lsp_cross", "elapsed_ms", itoa_buf((int)elapsed_ms(*t))); cbm_clock_gettime(CLOCK_MONOTONIC, t); diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index 85ef942b..a0663e10 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -89,6 +89,9 @@ CBMHashTable *cbm_pkgmap_build(cbm_pkg_entries_t *worker_entries, int worker_cou const char *project_name); /* Build pkgmap by reading manifest files from the files array (sequential path). */ +int cbm_pkgmap_scan_repo(const char *repo_path, cbm_pkg_entries_t *entries); +CBMHashTable *cbm_pkgmap_build_from_repo(const char *repo_path, const cbm_file_info_t *files, + int file_count, const char *project_name); CBMHashTable *cbm_pkgmap_build_from_files(const cbm_file_info_t *files, int file_count, const char *project_name);