diff --git a/src/python/cram/__main__.py b/src/python/cram/__main__.py index c250f2a..b2e22e3 100644 --- a/src/python/cram/__main__.py +++ b/src/python/cram/__main__.py @@ -154,25 +154,25 @@ def load_packages(root: Path, roots) -> dict: return packages -def missing_require_fatal(r): +def missing_require_fatal(r, e): log.fatal(f"Unable to load package {r}") _exit(1) -def missing_require_warn(r): +def missing_require_warn(r, e): log.warn(f"Unable to load package {r}") -def missing_require_ignore(r): - pass +def missing_require_ignore(r, e): + return -def build_fs(root: Path, roots, dest: Path, prelude: List[str], missing_require_handler) -> Vfs: +def build_fs( + root: Path, roots, dest: Path, prelude: List[str], missing_require_handler +) -> Vfs: """Build a VFS by configuring dest from the given config root.""" packages = load_packages(root, roots) - requirements = [] - requirements.extend(prelude) if packages: for p in packages: @@ -180,21 +180,23 @@ def build_fs(root: Path, roots, dest: Path, prelude: List[str], missing_require_ else: log.warning("Loaded no packages!") + requirements = list(prelude) for r in list(requirements): try: for d in packages[r].requires(): if d not in requirements: requirements.append(d) - except KeyError: - missing_require_handler(r) + except KeyError as e: + missing_require_handler(r, e) requirements.remove(r) + log.debug(f"Mapped requirements {prelude} to {requirements}") # Compute the topsort graph - requirements = {r: packages[r].requires() for r in requirements} + requirements_graph = {r: packages[r].requires() for r in requirements} fs = Vfs() # Abstractly execute the current packages - for r in toposort_flatten(requirements): + for r in toposort_flatten(requirements_graph): r = packages[r] r.install(fs, dest) @@ -304,6 +306,8 @@ def configure(ctx, param, filename: Optional[Path]): if error: exit(1) + pprint(ctx.default_map) + @click.group(invoke_without_command=True) @click.option( @@ -340,37 +344,72 @@ def cli(): pass +def arg_statefile(*args, **kwargs): + return click.option( + "--state-file", default=".cram.log", type=Path, callback=handle_path_vars + )(*args, **kwargs) + + +def arg_require(*args, **kwargs): + return click.option( + "--require", + type=str, + multiple=True, + default=["hosts.d/${HOSTNAME}", "profiles.d/default"], + callback=handle_requires, + )(*args, **kwargs) + + +def arg_require_root(*args, **kwargs): + return click.option( + "--require-root", + type=(str, click.Choice(["profile", "package"])), + multiple=True, + default=[("profiles.d", "profile"), ("hosts.d", "profile"), ("packages.d", "package")], + callback=lambda ctx, param, pairs: groupby( + [(y, expandvars(x)) for x, y in pairs], first, second + ), + )(*args, **kwargs) + + +def arg_missing_require(*args, **kwargs): + return click.option( + "--missing-require", + type=click.Choice(["error", "warn", "ignore"]), + callback=lambda ctx, param, it: { + "ignore": missing_require_ignore, + "warn": missing_require_warn, + "error": missing_require_fatal, + }[it], + )(*args, **kwargs) + + +def arg_optimize(*args, **kwargs): + return click.option("--optimize/--no-optimize", default=True)(*args, **kwargs) + + @cli.command("apply") @click.option("--execute/--dry-run", default=False) @click.option("--force/--no-force", default=False) -@click.option("--state-file", default=".cram.log", type=Path) -@click.option("--optimize/--no-optimize", default=True) -@click.option( - "--require", - type=str, - multiple=True, - default=[ - "hosts.d/${HOSTNAME}", - "profiles.d/default" - ], - callback=handle_requires, -) -@click.option( - "--require-root", - type=(str, click.Choice(["profile", "package"])), - multiple=True, - callback=lambda ctx, param, pairs: groupby([(y, expandvars(x)) for x, y in pairs], first, second) -) -@click.option( - "--missing-require", - type=click.Choice(["error", "warn", "ignore"]), - callback=lambda ctx, param, it: {"ignore": missing_require_ignore, "warn": missing_require_warn, "error": missing_require_fatal}[it] -) @click.option("--exec-idempotent/--exec-always", "exec_idempotent", default=True) +@arg_statefile +@arg_require +@arg_require_root +@arg_optimize +@arg_missing_require @click.argument("confdir", type=Path, callback=handle_path_vars) @click.argument("destdir", type=Path, callback=handle_path_vars) def do_apply( - confdir, destdir, state_file, execute, optimize, force, require, require_root, missing_require, exec_idempotent, + confdir, + destdir, + state_file, + execute, + optimize, + force, + require, + require_root, + missing_require, + exec_idempotent, ): """The entry point of cram.""" @@ -418,22 +457,15 @@ def do_apply( @cli.command("list") @click.option( - "-1", "--oneline", - is_flag=True, - default=False, - help="Only list names of resources" -) -@click.option( - "--require-root", - type=(str, click.Choice(["profile", "package"])), - multiple=True, - callback=lambda ctx, param, pairs: groupby([(y, expandvars(x)) for x, y in pairs], first, second) + "-1", "--oneline", is_flag=True, default=False, help="Only list names of resources" ) +@arg_require_root @click.argument("confdir", type=Path, callback=handle_path_vars) @click.argument("requirements", nargs=-1, callback=handle_requires) def do_list(confdir, requirements, require_root, oneline): """List out packages, profiles, hosts and subpackages in the .""" root = confdir.resolve() + log.debug(f"confdir: {root}, roots: {require_root}, requirements: {requirements}") if not root.is_dir(): log.fatal(f"{confdir} does not exist!") @@ -467,7 +499,7 @@ def do_list(confdir, requirements, require_root, oneline): @cli.command("state") -@click.option("--state-file", default=".cram.log", type=Path) +@arg_statefile @click.argument("confdir", type=Path, callback=handle_path_vars) def do_state(confdir, state_file): """List out the last `apply` state in the /.cram.log or --state-file.""" @@ -486,13 +518,8 @@ def do_state(confdir, state_file): @cli.command("fmt") +@arg_require_root @click.argument("confdir", type=Path, callback=handle_path_vars) -@click.option( - "--require-root", - type=(str, click.Choice(["profile", "package"])), - multiple=True, - callback=lambda ctx, param, pairs: groupby([(y, expandvars(x)) for x, y in pairs], first, second) -) @click.argument("requirement", type=str, callback=handle_path_vars) def do_fmt(confdir, requirement, require_root): """Format the specified requirement to a canonical-ish representation.""" @@ -523,14 +550,15 @@ def do_init(confdir: Path, force: bool): """Initialize an empty Cram repo.""" confdir.mkdir(parents=True, exist_ok=True) - rootfile = confdir / 'cram.toml' + rootfile = confdir / "cram.toml" if rootfile.exists() and not force: log.fatal("cram.toml exists! Provide -f/--force to overwrite") - exit(1) + _exit(1) log.info("Writing initial cram.toml...") - with open(rootfile, 'w') as fp: - fp.write("""\ + with open(rootfile, "w") as fp: + fp.write( + """\ [cram] version = 1 @@ -540,22 +568,27 @@ destdir = "${HOME}" state_file = "${PWD}/.cram.log" optimize = true exec_idempotent = true -require_roots = { - "packages.d" = "package", - "profiles.d" = "profile" - "hosts.d" = "profile", -} +# Where to load requirements from, and the requirement type +# Types: package, profile +require_root = [ + ["${PWD}/packages.d", "package"], + ["${PWD}/profiles.d", "profile"], + ["${PWD}/hosts.d", "profile"], +] +# Choice([error, warn, ignore]) missing_require = "error" [cram.task.apply] +missing_require = "ignore" require = [ "hosts.d/${FQDN}", "hosts.d/${HOSTNAME}", "profiles.d/default", ] -""") +""" + ) - for d in ['profiles.d', 'packages.d', 'hosts.d']: + for d in ["profiles.d", "packages.d", "hosts.d"]: (confdir / d).mkdir(parents=True, exist_ok=True) diff --git a/test/integration/cram.toml b/test/integration/cram.toml index bcfecb7..c38d62c 100644 --- a/test/integration/cram.toml +++ b/test/integration/cram.toml @@ -20,7 +20,8 @@ missing_require = "error" [cram.task.apply] missing_require = "ignore" require = [ - "hosts.d/${FQDN}", - "hosts.d/${HOSTNAME}", +# "hosts.d/${FQDN}", +# "hosts.d/${HOSTNAME}", + "hosts.d/test", "profiles.d/default", ]