Commit 43cbb229f4359944ef3d106752f7ac004b2d6649
Commits[COMMIT BEGIN]commit 43cbb229f4359944ef3d106752f7ac004b2d6649 Author: 0x4248 <[email protected]> Date: Fri Mar 6 21:59:49 2026 +0000 npkg: redo package system again diff --git a/.gitignore b/.gitignore index 9d37de3..2da91e4 100644 --- a/.gitignore +++ b/.gitignore @@ -63,9 +63,12 @@ COMMIT_MSG.old # C !*.c !*.h +!*.C +!*.H # C++ !*.cpp +!*.CPP !*.c++ !*.hpp !*.h++ diff --git a/npkg-testing/README.md b/npkg-testing/README.md index 2ea743a..8009e69 100644 --- a/npkg-testing/README.md +++ b/npkg-testing/README.md @@ -18,7 +18,7 @@ Code and package folders live directly here so development stays simple. ## Demo Package -`bin/hello_world` builds a C binary and packages it as `/bin/hello-world`. +`bin/hello_world` builds a C binary and packages it as `/usr/bin/hello-world`. ## Commands @@ -28,11 +28,16 @@ From repository root: ./npkg list ./npkg installed ./npkg build hello-world +./npkg package hello-world ./npkg install hello-world -./npkg install-prebuilt --file ./npkg-build/packages/hello-world-0.1.0.tar.gz +./npkg clean hello-world ./npkg uninstall hello-world ``` +`build` compiles package sources only. +`package` creates `npkg-build/packages/<name>-<version>.tar.gz`. + +Default install root is `/opt/npkg`, so installs land under `/opt/npkg/usr/...`. If `/opt/npkg` is not writable for your user, run install/uninstall with `sudo`. To run as `npkg ...` directly, symlink once: diff --git a/npkg-testing/bin/hello_python/Makefile b/npkg-testing/bin/hello_python/Makefile new file mode 100644 index 0000000..73f85e7 --- /dev/null +++ b/npkg-testing/bin/hello_python/Makefile @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: GPL-3.0 + +BIN = hello-python +DESTDIR ?= +INSTALL_PATH ?= /usr/bin/ + +.PHONY: all install clean + +all: + chmod +x $(BIN) + +install: all + install -d $(DESTDIR)$(INSTALL_PATH) + install -m 0755 $(BIN) $(DESTDIR)$(INSTALL_PATH)$(BIN) + +clean: + true diff --git a/npkg-testing/bin/hello_python/npkg.conf b/npkg-testing/bin/hello_python/npkg.conf new file mode 100644 index 0000000..4fe9e16 --- /dev/null +++ b/npkg-testing/bin/hello_python/npkg.conf @@ -0,0 +1,13 @@ +name = "hello-python" +version = "0.1.0" +description = "Tiny demo Python package that installs /usr/bin/hello-python" +install_path = "/usr/bin/" + +[build] +command = "make -C {package_dir} all" + +[package] +command = "make -C {package_dir} install DESTDIR={stage_dir} INSTALL_PATH={install_path}" + +[clean] +command = "make -C {package_dir} clean" diff --git a/npkg-testing/bin/hello_world/Makefile b/npkg-testing/bin/hello_world/Makefile index f674546..4d4fd08 100644 --- a/npkg-testing/bin/hello_world/Makefile +++ b/npkg-testing/bin/hello_world/Makefile @@ -6,8 +6,8 @@ CFLAGS ?= -O2 -Wall PKG_NAME = hello-world PKG_VERSION = 0.1.0 -PREFIX ?= /usr DESTDIR ?= +INSTALL_PATH ?= /usr/bin/ SRC = src/hello_world.c BUILD_DIR = build @@ -22,8 +22,8 @@ $(BUILD_DIR)/$(BIN): $(SRC) $(CC) $(CFLAGS) $(SRC) -o $(BUILD_DIR)/$(BIN) install: all - install -d $(DESTDIR)$(PREFIX)/bin - install -m 0755 $(BUILD_DIR)/$(BIN) $(DESTDIR)$(PREFIX)/bin/$(BIN) + install -d $(DESTDIR)$(INSTALL_PATH) + install -m 0755 $(BUILD_DIR)/$(BIN) $(DESTDIR)$(INSTALL_PATH)$(BIN) clean: rm -rf $(BUILD_DIR) diff --git a/npkg-testing/bin/hello_world/npkg.conf b/npkg-testing/bin/hello_world/npkg.conf index 5412258..f60733f 100644 --- a/npkg-testing/bin/hello_world/npkg.conf +++ b/npkg-testing/bin/hello_world/npkg.conf @@ -1,16 +1,13 @@ name = "hello-world" version = "0.1.0" -description = "Tiny demo C package that installs /bin/hello-world" +description = "Tiny demo C package that installs /usr/bin/hello-world" +install_path = "/usr/bin/" [build] command = "make -C {package_dir} all" -[stage] -command = "make -C {package_dir} install DESTDIR={stage_dir} PREFIX={prefix}" +[package] +command = "make -C {package_dir} install DESTDIR={stage_dir} INSTALL_PATH={install_path}" [clean] -command = "make -C {package_dir} clean" - -[capabilities] -installable = true -cleanable = true \ No newline at end of file +command = "make -C {package_dir} clean" \ No newline at end of file diff --git a/npkg-testing/npkg-build/README.md b/npkg-testing/npkg-build/README.md deleted file mode 100644 index a382c82..0000000 --- a/npkg-testing/npkg-build/README.md +++ /dev/null @@ -1,89 +0,0 @@ -# NPKG tools - -Package tooling lives here. - -Implementation modules are in `tools/npkg/*.py` and loaded by the root `./npkg` entrypoint. - -- `../npkg` - CLI entrypoint (`list`, `build`, `install`, `uninstall`) -- `work/` - staging work dirs -- `packages/` - built package archives - -## Quick start - -```bash -./npkg list -./npkg installed -./npkg build hello-world -./npkg install hello-world -./npkg install-prebuilt --file ./npkg-build/packages/hello-world-0.1.0.tar.gz -./npkg uninstall hello-world -``` - -Default install root is `/opt/npkg` and default prefix is `/` (so binaries land under `/opt/npkg/bin`, etc). -If `/opt/npkg` is not writable for your user, run install/uninstall with `sudo`. - -## Package metadata - -Packages are discovered by scanning these directories for one metadata file per package directory: - -- `npkg.conf` (preferred) -- `npkg.ini` -- `npkg.toml` (compatibility) -- `npkg.json` (legacy compatibility) - -If more than one metadata file exists in a package directory, `npkg` errors to avoid ambiguity. - -Scan roots: - -- `bin/` -- `sbin/` -- `toolkits/` -- `lib/public/` -- `lib/private/` -- `lab/` -- `systems/` - -Minimal `npkg.conf` example: - -```toml -name = "hello-world" -version = "0.1.0" -description = "Tiny demo C package" - -[build] -command = "make -C {package_dir} all" - -[stage] -command = "make -C {package_dir} install DESTDIR={stage_dir} PREFIX={prefix}" - -[capabilities] -installable = true -``` - -INI equivalent: - -```ini -[package] -name = hello-world -version = 0.1.0 -description = Tiny demo C package - -[build] -command = make -C {package_dir} all - -[stage] -command = make -C {package_dir} install DESTDIR={stage_dir} PREFIX={prefix} - -[capabilities] -installable = true -``` - -Notes: - -- Optional defaults: - - `name` defaults to folder name (with `_` converted to `-`) - - `version` defaults to `0.1.0` - - `description` defaults to empty - - `capabilities.installable` defaults to `true` if `stage.command` is set, else `false` -- `build.command` remains optional. -- `stage.command` is required only for installable packages. diff --git a/npkg-testing/tools/npkg/archive.py b/npkg-testing/tools/npkg/archive.py index 12c4dc6..88a34b6 100644 --- a/npkg-testing/tools/npkg/archive.py +++ b/npkg-testing/tools/npkg/archive.py @@ -1,4 +1,3 @@ -import re import shutil import subprocess import tarfile @@ -16,6 +15,7 @@ def command_context(pkg: Package, stage_dir: Optional[Path] = None) -> Dict[str, "npkg_root": str(workspace_root()), "npkg_build_root": str(workspace_root() / "npkg-build"), "package_dir": str(pkg.package_dir), + "install_path": pkg.install_path, } if stage_dir is not None: context["stage_dir"] = str(stage_dir) @@ -32,11 +32,9 @@ def run_shell(command: str, cwd: Path) -> None: raise RuntimeError(f"Command failed with exit code {result.returncode}: {command}") -def ensure_package_archive(pkg: Package, prefix: str) -> Path: - if not pkg.installable: - raise RuntimeError(f"Package '{pkg.name}' is not installable") - if not pkg.stage_command: - raise RuntimeError(f"Package '{pkg.name}' does not define stage.command") +def ensure_package_archive(pkg: Package) -> Path: + if not pkg.package_command: + raise RuntimeError(f"Package '{pkg.name}' does not define package.command") stage_dir = package_stage_dir(pkg) archive_path = package_archive_path(pkg) @@ -45,11 +43,10 @@ def ensure_package_archive(pkg: Package, prefix: str) -> Path: stage_dir.mkdir(parents=True, exist_ok=True) context = command_context(pkg, stage_dir=stage_dir) - context["prefix"] = prefix - stage_command = render_command(pkg.stage_command, context) + package_command = render_command(pkg.package_command, context) - info(f"staging {pkg.name} with: {stage_command}") - run_shell(stage_command, cwd=workspace_root()) + info(f"staging {pkg.name} with: {package_command}") + run_shell(package_command, cwd=workspace_root()) archive_path.parent.mkdir(parents=True, exist_ok=True) with tarfile.open(archive_path, "w:gz") as tar: @@ -71,19 +68,3 @@ def extract_archive_into_root(archive_path: Path, install_root: Path) -> List[st except (tarfile.TarError, OSError) as error: raise RuntimeError(f"failed to install archive: {error}") from error return member_names - - -def infer_package_from_archive(archive_path: Path) -> tuple[str, str]: - base = archive_path.name - if base.endswith(".tar.gz"): - base = base[:-7] - elif base.endswith(".tgz"): - base = base[:-4] - else: - base = archive_path.stem - - match = re.match(r"^(?P<name>.+)-(?P<version>\d+\.\d+\.\d+.*)$", base) - if match: - return match.group("name"), match.group("version") - - return base, "prebuilt" diff --git a/npkg-testing/tools/npkg/cli.py b/npkg-testing/tools/npkg/cli.py index f472665..81d513c 100644 --- a/npkg-testing/tools/npkg/cli.py +++ b/npkg-testing/tools/npkg/cli.py @@ -2,10 +2,11 @@ import argparse from .commands import ( cmd_build, + cmd_clean, cmd_install, - cmd_install_prebuilt, cmd_installed, cmd_list, + cmd_package, cmd_uninstall, ) from .console import fail @@ -14,15 +15,17 @@ from .console import fail def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="npkg", - description="Nexus package helper", + description="Simple package helper for the Nexus monorepo", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( "Examples:\n" " npkg list\n" - " npkg installed\n" " npkg build hello-world\n" + " npkg build '*'\n" + " npkg package hello-world\n" " npkg install hello-world\n" - " npkg install-prebuilt --file ./npkg-build/packages/hello-world-0.1.0.tar.gz\n" + " npkg clean '*'\n" + " npkg installed\n" " npkg uninstall hello-world" ), ) @@ -32,18 +35,22 @@ def build_parser() -> argparse.ArgumentParser: list_parser = sub.add_parser("list", help="List available packages") list_parser.set_defaults(func=cmd_list) - installed_parser = sub.add_parser("installed", help="List installed packages from manifest database") - installed_parser.add_argument( - "--root", - default="/opt/npkg", - help="Installation root directory (default: /opt/npkg)", + build_parser = sub.add_parser("build", help="Build package(s) (supports wildcards)") + build_parser.add_argument( + "packages", + nargs="+", + help="Package selectors (name/path or wildcards like '*', 'hello-*', 'bin/*')", ) - installed_parser.set_defaults(func=cmd_installed) - - build_parser = sub.add_parser("build", help="Build a package") - build_parser.add_argument("package", help="Package name or package directory path") build_parser.set_defaults(func=cmd_build) + package_parser = sub.add_parser("package", help="Create package archive(s) (.tar.gz, supports wildcards)") + package_parser.add_argument( + "packages", + nargs="+", + help="Package selectors (name/path or wildcards like '*', 'hello-*', 'bin/*')", + ) + package_parser.set_defaults(func=cmd_package) + install_parser = sub.add_parser("install", help="Install a package") install_parser.add_argument("package", help="Package name or package directory path") install_parser.add_argument( @@ -51,29 +58,23 @@ def build_parser() -> argparse.ArgumentParser: default="/opt/npkg", help="Installation root directory (default: /opt/npkg)", ) - install_parser.add_argument( - "--prefix", - default="/", - help="Prefix passed to package stage command (default: /)", - ) install_parser.set_defaults(func=cmd_install) - install_prebuilt_parser = sub.add_parser("install-prebuilt", help="Install from a prebuilt tarball") - install_prebuilt_parser.add_argument( - "--file", - required=True, - help="Path to .tar.gz or .tgz package archive", - ) - install_prebuilt_parser.add_argument( - "--package", - help="Package name override (defaults to name inferred from archive filename)", + clean_parser = sub.add_parser("clean", help="Run package clean command (supports wildcards)") + clean_parser.add_argument( + "packages", + nargs="+", + help="Package selectors (name/path or wildcards like '*', 'hello-*', 'bin/*')", ) - install_prebuilt_parser.add_argument( + clean_parser.set_defaults(func=cmd_clean) + + installed_parser = sub.add_parser("installed", help="List installed packages from manifest database") + installed_parser.add_argument( "--root", default="/opt/npkg", help="Installation root directory (default: /opt/npkg)", ) - install_prebuilt_parser.set_defaults(func=cmd_install_prebuilt) + installed_parser.set_defaults(func=cmd_installed) uninstall_parser = sub.add_parser("uninstall", help="Uninstall a package") uninstall_parser.add_argument("package", help="Package name") diff --git a/npkg-testing/tools/npkg/commands.py b/npkg-testing/tools/npkg/commands.py index dd09ed2..4b9d742 100644 --- a/npkg-testing/tools/npkg/commands.py +++ b/npkg-testing/tools/npkg/commands.py @@ -1,12 +1,59 @@ from pathlib import Path +import fnmatch -from .archive import command_context, ensure_package_archive, extract_archive_into_root, infer_package_from_archive, render_command, run_shell +from .archive import command_context, ensure_package_archive, extract_archive_into_root, render_command, run_shell from .console import fail, info, ok, style, warn -from .install_db import load_installed_rows, uninstall_from_manifest, write_install_manifest, write_prebuilt_manifest +from .install_db import load_installed_rows, uninstall_from_manifest, write_install_manifest from .metadata import discover_packages, select_package from .paths import workspace_root +def resolve_package_selectors(packages, selectors): + root = workspace_root() + matched: list = [] + seen = set() + had_selector_miss = False + + for selector in selectors: + selector = selector.strip() + if not selector: + continue + + wildcard = any(token in selector for token in "*?[") + local_matches = [] + + if wildcard: + for pkg in packages.values(): + rel_dir = str(pkg.package_dir.relative_to(root)) + if fnmatch.fnmatch(pkg.name, selector) or fnmatch.fnmatch(rel_dir, selector): + local_matches.append(pkg) + else: + try: + local_matches.append(select_package(packages, selector)) + except KeyError: + normalized_selector = selector.strip().strip("/") + for pkg in packages.values(): + rel_dir = str(pkg.package_dir.relative_to(root)) + if rel_dir == normalized_selector or rel_dir.startswith(f"{normalized_selector}/"): + local_matches.append(pkg) + + if not local_matches: + normalized_selector = selector.strip().strip("/") + selector_exists = (root / normalized_selector).exists() if normalized_selector else False + if wildcard or not selector_exists: + warn(f"no packages matched selector: {selector}") + had_selector_miss = True + continue + + for pkg in local_matches: + if pkg.name in seen: + continue + seen.add(pkg.name) + matched.append(pkg) + + return matched, had_selector_miss + + def cmd_list(_args) -> int: packages = discover_packages() print(style("NPKG packages", bold=True)) @@ -16,13 +63,14 @@ def cmd_list(_args) -> int: for pkg in packages.values(): flags = [] - flags.append("build" if pkg.build_command else "no-build") - if pkg.installable and pkg.stage_command: + if pkg.build_command: + flags.append("build") + if pkg.package_command: flags.append("install") - elif pkg.installable and not pkg.stage_command: - flags.append("no-stage") - else: - flags.append("build-only") + if pkg.clean_command: + flags.append("clean") + if not flags: + flags.append("meta-only") rel_dir = pkg.package_dir.relative_to(workspace_root()) print( @@ -37,26 +85,49 @@ def cmd_list(_args) -> int: def cmd_build(args) -> int: packages = discover_packages() - try: - pkg = select_package(packages, args.package) - except KeyError: - fail(f"package not found: {args.package}") + matched, had_selector_miss = resolve_package_selectors(packages, args.packages) + if not matched: return 2 - if not pkg.build_command: - fail(f"package '{pkg.name}' does not define build.command") + had_failures = False + for pkg in matched: + if not pkg.build_command: + warn(f"skipping {pkg.name}: build.command is not defined") + had_failures = True + continue + + command = render_command(pkg.build_command, command_context(pkg)) + info(f"building {pkg.name} with: {command}") + try: + run_shell(command, cwd=workspace_root()) + except RuntimeError as error: + warn(f"failed to build {pkg.name}: {error}") + had_failures = True + continue + + ok(f"built {pkg.name}") + + return 1 if (had_failures or had_selector_miss) else 0 + + +def cmd_package(args) -> int: + packages = discover_packages() + matched, had_selector_miss = resolve_package_selectors(packages, args.packages) + if not matched: return 2 - command = render_command(pkg.build_command, command_context(pkg)) - info(f"building {pkg.name} with: {command}") - try: - run_shell(command, cwd=workspace_root()) - except RuntimeError as error: - fail(str(error)) - return 1 + had_failures = False + for pkg in matched: + try: + archive_path = ensure_package_archive(pkg) + except RuntimeError as error: + warn(f"failed to package {pkg.name}: {error}") + had_failures = True + continue - ok(f"built {pkg.name}") - return 0 + ok(f"package archive ready: {archive_path}") + + return 1 if (had_failures or had_selector_miss) else 0 def cmd_install(args) -> int: @@ -68,10 +139,8 @@ def cmd_install(args) -> int: return 2 install_root = Path(args.root).expanduser().resolve() - prefix = args.prefix - try: - archive_path = ensure_package_archive(pkg, prefix=prefix) + archive_path = ensure_package_archive(pkg) except RuntimeError as error: fail(str(error)) return 2 @@ -85,45 +154,36 @@ def cmd_install(args) -> int: return 1 write_install_manifest(pkg, install_root, member_names) - ok(f"installed {pkg.name} -> {install_root}") return 0 -def cmd_install_prebuilt(args) -> int: - archive_path = Path(args.file).expanduser().resolve() - if not archive_path.exists() or not archive_path.is_file(): - fail(f"archive not found: {archive_path}") +def cmd_clean(args) -> int: + packages = discover_packages() + root = workspace_root() + matched, had_selector_miss = resolve_package_selectors(packages, args.packages) + if not matched: return 2 - install_root = Path(args.root).expanduser().resolve() - install_root.mkdir(parents=True, exist_ok=True) - - inferred_name, inferred_version = infer_package_from_archive(archive_path) - package_name = (args.package or inferred_name).strip() - package_version = inferred_version - - if not package_name: - fail("unable to determine package name; pass --package") - return 2 + had_failures = False + for pkg in matched: + if not pkg.clean_command: + warn(f"skipping {pkg.name}: clean.command is not defined") + had_failures = True + continue - info(f"installing prebuilt {package_name} from {archive_path} into {install_root}") - try: - member_names = extract_archive_into_root(archive_path, install_root) - except RuntimeError as error: - fail(str(error)) - return 1 + command = render_command(pkg.clean_command, command_context(pkg)) + info(f"cleaning {pkg.name} with: {command}") + try: + run_shell(command, cwd=root) + except RuntimeError as error: + warn(f"failed to clean {pkg.name}: {error}") + had_failures = True + continue - write_prebuilt_manifest( - package_name=package_name, - version=package_version, - install_root=install_root, - members=member_names, - source_archive=archive_path, - ) + ok(f"cleaned {pkg.name}") - ok(f"installed prebuilt {package_name} -> {install_root}") - return 0 + return 1 if (had_failures or had_selector_miss) else 0 def cmd_uninstall(args) -> int: diff --git a/npkg-testing/tools/npkg/install_db.py b/npkg-testing/tools/npkg/install_db.py index e9be84b..0434a7a 100644 --- a/npkg-testing/tools/npkg/install_db.py +++ b/npkg-testing/tools/npkg/install_db.py @@ -20,27 +20,6 @@ def write_install_manifest(pkg: Package, install_root: Path, members: List[str]) json.dump(manifest, handle, indent=2) -def write_prebuilt_manifest( - package_name: str, - version: str, - install_root: Path, - members: List[str], - source_archive: Path, -) -> None: - db_dir = manifest_root(install_root) - db_dir.mkdir(parents=True, exist_ok=True) - manifest = { - "package": package_name, - "version": version, - "install_root": str(install_root), - "source_archive": str(source_archive), - "paths": members, - } - manifest_path = package_manifest_path(package_name, install_root) - with manifest_path.open("w", encoding="utf-8") as handle: - json.dump(manifest, handle, indent=2) - - def read_install_manifest(pkg_name: str, install_root: Path) -> Dict[str, Any]: manifest_path = package_manifest_path(pkg_name, install_root) if not manifest_path.exists(): diff --git a/npkg-testing/tools/npkg/metadata.py b/npkg-testing/tools/npkg/metadata.py index f5cc75b..ed048cd 100644 --- a/npkg-testing/tools/npkg/metadata.py +++ b/npkg-testing/tools/npkg/metadata.py @@ -1,12 +1,10 @@ -import configparser -import json import os from pathlib import Path from typing import Any, Dict, Optional try: import tomllib -except ModuleNotFoundError: +except ModuleNotFoundError: # pragma: no cover tomllib = None from .paths import workspace_root @@ -17,36 +15,14 @@ def normalize_command(raw: Optional[str]) -> Optional[str]: if raw is None: return None command = raw.strip() - if not command: - return None - return command + return command or None def as_dict(value: Any) -> Dict[str, Any]: return value if isinstance(value, dict) else {} -def parse_bool(value: Any, default: bool) -> bool: - if value is None: - return default - if isinstance(value, bool): - return value - if isinstance(value, (int, float)): - return bool(value) - raw = str(value).strip().lower() - if raw in {"1", "true", "yes", "on"}: - return True - if raw in {"0", "false", "no", "off"}: - return False - return default - - -def load_json_package(meta_path: Path) -> Dict[str, Any]: - with meta_path.open("r", encoding="utf-8") as handle: - return json.load(handle) - - -def load_toml_package(meta_path: Path) -> Dict[str, Any]: +def load_raw_package(meta_path: Path) -> Dict[str, Any]: if tomllib is None: raise ValueError("TOML metadata requires Python 3.11+ (tomllib not available)") with meta_path.open("rb") as handle: @@ -56,61 +32,6 @@ def load_toml_package(meta_path: Path) -> Dict[str, Any]: return parsed -def load_ini_package(meta_path: Path) -> Dict[str, Any]: - parser = configparser.ConfigParser() - parser.optionxform = str - read_ok = parser.read(meta_path, encoding="utf-8") - if not read_ok: - raise ValueError(f"Unable to read metadata: {meta_path}") - - package: Dict[str, Any] = {} - build: Dict[str, Any] = {} - stage: Dict[str, Any] = {} - capabilities: Dict[str, Any] = {} - - if parser.has_section("package"): - section = parser["package"] - package["name"] = section.get("name") - package["version"] = section.get("version") - package["description"] = section.get("description") - - if parser.has_section("build"): - build["command"] = parser["build"].get("command") - - if parser.has_section("stage"): - stage["command"] = parser["stage"].get("command") - - if parser.has_section("capabilities"): - capabilities["installable"] = parser["capabilities"].get("installable") - - package["build"] = build - package["stage"] = stage - package["capabilities"] = capabilities - return package - - -def load_raw_package(meta_path: Path) -> Dict[str, Any]: - name = meta_path.name.lower() - suffix = meta_path.suffix.lower() - if name == "npkg.conf": - try: - data = load_toml_package(meta_path) - except ValueError: - data = load_ini_package(meta_path) - elif suffix == ".json": - data = load_json_package(meta_path) - elif suffix == ".toml": - data = load_toml_package(meta_path) - elif suffix == ".ini": - data = load_ini_package(meta_path) - else: - raise ValueError(f"Unsupported metadata format: {meta_path.name}") - - if not isinstance(data, dict): - raise ValueError(f"Invalid metadata in {meta_path}: expected object/table") - return data - - def load_package(meta_path: Path) -> Package: data = load_raw_package(meta_path) @@ -118,41 +39,33 @@ def load_package(meta_path: Path) -> Package: name = str(data.get("name") or default_name).strip() version = str(data.get("version") or "0.1.0").strip() description = str(data.get("description") or "").strip() + install_path = str(data.get("install_path") or "/usr/bin/").strip() + build = as_dict(data.get("build", {})) - stage = as_dict(data.get("stage", {})) - capabilities = as_dict(data.get("capabilities", {})) + package_section = as_dict(data.get("package", {})) + clean = as_dict(data.get("clean", {})) if not name or not version: raise ValueError(f"Invalid metadata in {meta_path}: missing name/version") - build_command = normalize_command(build.get("command")) - stage_command = normalize_command(stage.get("command")) - installable_default = stage_command is not None - installable = parse_bool(capabilities.get("installable"), default=installable_default) + if install_path and not install_path.startswith("/"): + install_path = f"/{install_path}" return Package( name=name, version=version, description=description, package_dir=meta_path.parent, - build_command=build_command, - stage_command=stage_command, - installable=installable, + install_path=install_path, + build_command=normalize_command(build.get("command")), + package_command=normalize_command(package_section.get("command")), + clean_command=normalize_command(clean.get("command")), ) def metadata_file_in_dir(package_dir: Path) -> Optional[Path]: - candidates = [ - package_dir / "npkg.conf", - package_dir / "npkg.toml", - package_dir / "npkg.ini", - package_dir / "npkg.json", - ] - found = [path for path in candidates if path.exists()] - if len(found) > 1: - names = ", ".join(path.name for path in found) - raise ValueError(f"Multiple metadata files in {package_dir}: {names}") - return found[0] if found else None + meta = package_dir / "npkg.conf" + return meta if meta.exists() else None def discover_packages() -> Dict[str, Package]: @@ -172,8 +85,7 @@ def discover_packages() -> Dict[str, Package]: if not top.exists(): continue for dirpath, _, filenames in os.walk(top): - filename_set = set(filenames) - if not ({"npkg.conf", "npkg.toml", "npkg.ini", "npkg.json"} & filename_set): + if "npkg.conf" not in set(filenames): continue package_dir = Path(dirpath) meta = metadata_file_in_dir(package_dir) diff --git a/npkg-testing/tools/npkg/types.py b/npkg-testing/tools/npkg/types.py index 3d31325..b43c8a2 100644 --- a/npkg-testing/tools/npkg/types.py +++ b/npkg-testing/tools/npkg/types.py @@ -9,6 +9,7 @@ class Package: version: str description: str package_dir: Path + install_path: str build_command: Optional[str] - stage_command: Optional[str] - installable: bool + package_command: Optional[str] + clean_command: Optional[str][COMMIT END](C) 2025 0x4248 (C) 2025 4248 Media and 4248 Systems, All part of 0x4248 See LICENCE files for more information. Not all files are by 0x4248 always check Licencing.