From f2db543651583aa160c67e74ce2d88c17e32156c Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sun, 6 Jul 2025 13:31:46 +0200 Subject: [PATCH] Templates: migrate clan templates to flake identifiers --- pkgs/clan-cli/clan_cli/clan/create.py | 36 ++--------- pkgs/clan-cli/clan_lib/clan/create.py | 67 ++++++++------------ pkgs/clan-cli/clan_lib/templates/handler.py | 70 +++++++++++++++++++++ pkgs/clan-cli/clan_lib/tests/test_create.py | 2 +- 4 files changed, 100 insertions(+), 75 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/clan/create.py b/pkgs/clan-cli/clan_cli/clan/create.py index 2604456e0..8d8cf2def 100644 --- a/pkgs/clan-cli/clan_cli/clan/create.py +++ b/pkgs/clan-cli/clan_cli/clan/create.py @@ -4,36 +4,17 @@ import logging from pathlib import Path from clan_lib.clan.create import CreateOptions, create_clan -from clan_lib.templates import ( - InputPrio, -) log = logging.getLogger(__name__) def register_create_parser(parser: argparse.ArgumentParser) -> None: - parser.add_argument( - "--input", - type=str, - help="""Flake input name to use as template source - can be specified multiple times, inputs are tried in order of definition - Example: --input clan --input clan-core - """, - action="append", - default=[], - ) - - parser.add_argument( - "--no-self", - help="Do not look into own flake for templates", - action="store_true", - default=False, - ) - parser.add_argument( "--template", type=str, - help="Clan template name", + help="""Reference to the template to use for the clan. default="default". In the format '#template_name' Where is a flake reference (e.g. github:org/repo) or a local path (e.g. '.' ). + Omitting '#' will use the builtin templates (e.g. just 'default' from clan-core ). + """, default="default", ) @@ -59,19 +40,10 @@ def register_create_parser(parser: argparse.ArgumentParser) -> None: ) def create_flake_command(args: argparse.Namespace) -> None: - if len(args.input) == 0: - args.input = ["clan", "clan-core"] - - if args.no_self: - input_prio = InputPrio.try_inputs(tuple(args.input)) - else: - input_prio = InputPrio.try_self_then_inputs(tuple(args.input)) - create_clan( CreateOptions( - input_prio=input_prio, dest=args.path, - template_name=args.template, + template=args.template, setup_git=not args.no_git, src_flake=args.flake, update_clan=not args.no_update, diff --git a/pkgs/clan-cli/clan_lib/clan/create.py b/pkgs/clan-cli/clan_lib/clan/create.py index ddea36969..81272977b 100644 --- a/pkgs/clan-cli/clan_lib/clan/create.py +++ b/pkgs/clan-cli/clan_lib/clan/create.py @@ -4,16 +4,12 @@ from pathlib import Path from clan_lib.api import API from clan_lib.cmd import RunOpts, run +from clan_lib.dirs import clan_templates from clan_lib.errors import ClanError from clan_lib.flake import Flake from clan_lib.nix import nix_command, nix_metadata, nix_shell from clan_lib.persist.inventory_store import InventorySnapshot, InventoryStore -from clan_lib.templates import ( - InputPrio, - TemplateName, - get_template, -) -from clan_lib.templates.filesystem import copy_from_nixstore +from clan_lib.templates.handler import clan_template log = logging.getLogger(__name__) @@ -21,9 +17,9 @@ log = logging.getLogger(__name__) @dataclass class CreateOptions: dest: Path - template_name: str + template: str + src_flake: Flake | None = None - input_prio: InputPrio | None = None setup_git: bool = True initial: InventorySnapshot | None = None update_clan: bool = True @@ -47,44 +43,31 @@ def create_clan(opts: CreateOptions) -> None: log.warning("Setting src_flake to None") opts.src_flake = None - template = get_template( - TemplateName(opts.template_name), - "clan", - input_prio=opts.input_prio, - clan_dir=opts.src_flake, - ) - log.info(f"Found template '{template.name}' in '{template.input_variant}'") + if opts.src_flake is None: + opts.src_flake = Flake(str(clan_templates())) - if dest.exists(): - dest /= template.name + with clan_template( + opts.src_flake, template_ident=opts.template, dst_dir=opts.dest + ) as _clan_dir: + if opts.setup_git: + run(git_command(dest, "init")) + run(git_command(dest, "add", ".")) - if dest.exists(): - msg = f"Destination directory {dest} already exists" - raise ClanError(msg) + # check if username is set + has_username = run( + git_command(dest, "config", "user.name"), RunOpts(check=False) + ) + if has_username.returncode != 0: + run(git_command(dest, "config", "user.name", "clan-tool")) - src = Path(template.src["path"]) + has_username = run( + git_command(dest, "config", "user.email"), RunOpts(check=False) + ) + if has_username.returncode != 0: + run(git_command(dest, "config", "user.email", "clan@example.com")) - copy_from_nixstore(src, dest) - - if opts.setup_git: - run(git_command(dest, "init")) - run(git_command(dest, "add", ".")) - - # check if username is set - has_username = run( - git_command(dest, "config", "user.name"), RunOpts(check=False) - ) - if has_username.returncode != 0: - run(git_command(dest, "config", "user.name", "clan-tool")) - - has_username = run( - git_command(dest, "config", "user.email"), RunOpts(check=False) - ) - if has_username.returncode != 0: - run(git_command(dest, "config", "user.email", "clan@example.com")) - - if opts.update_clan: - run(nix_command(["flake", "update"]), RunOpts(cwd=dest)) + if opts.update_clan: + run(nix_command(["flake", "update"]), RunOpts(cwd=dest)) if opts.initial: inventory_store = InventoryStore(flake=Flake(str(opts.dest))) diff --git a/pkgs/clan-cli/clan_lib/templates/handler.py b/pkgs/clan-cli/clan_lib/templates/handler.py index 185552243..dd44f0438 100644 --- a/pkgs/clan-cli/clan_lib/templates/handler.py +++ b/pkgs/clan-cli/clan_lib/templates/handler.py @@ -99,3 +99,73 @@ def machine_template( finally: # If no error occurred, the machine directory is kept pass + + +@contextmanager +def clan_template(flake: Flake, template_ident: str, dst_dir: Path) -> Iterator[Path]: + """ + Create a clan from a template. + This function will copy the template files to a new clan directory + + :param flake: The flake to create the machine in. + :param template_ident: The identifier of the template to use. Example ".#template_name" + :param dst: The name of the directory to create. + + + Example usage: + + >>> with clan_template( + ... Flake("/home/johannes/git/clan-core"), ".#new-machine", "my-machine" + ... ) as clan_dir: + ... # Use `clan_dir` here if you want to access the created directory + + ... The directory is removed if the context raised any errors. + ... Only if the context is exited without errors, it is kept. + """ + + # Get the clan template from the specifier + [flake_ref, template_selector] = transform_url("clan", template_ident, flake=flake) + # For pretty error messages + printable_template_ref = f"{flake_ref}#{template_selector}" + + template_flake = Flake(flake_ref) + + try: + template = template_flake.select(template_selector) + except ClanError as e: + msg = f"Failed to select template '{template_ident}' from flake '{flake_ref}' (via attribute path: {printable_template_ref})" + raise ClanError(msg) from e + + src = template.get("path") + if not src: + msg = f"Malformed template: {printable_template_ref} does not have a 'path' attribute" + raise ClanError(msg) + + src_path = Path(src).resolve() + + realize_nix_path(template_flake, str(src_path)) + + if not src_path.exists(): + msg = f"Template {printable_template_ref} does not exist at {src_path}" + raise ClanError(msg) + + if not src_path.is_dir(): + msg = f"Template {printable_template_ref} is not a directory at {src_path}" + raise ClanError(msg) + + if dst_dir.exists(): + msg = f"Destination directory {dst_dir} already exists" + raise ClanError(msg) + + copy_from_nixstore(src_path, dst_dir) + + try: + yield dst_dir + except Exception as e: + log.error(f"An error occurred inside the 'clan_template' context: {e}") + log.info(f"Removing left-over directory: {dst_dir}") + shutil.rmtree(dst_dir, ignore_errors=True) + raise + finally: + # If no error occurred, the directory is kept + pass diff --git a/pkgs/clan-cli/clan_lib/tests/test_create.py b/pkgs/clan-cli/clan_lib/tests/test_create.py index 29b460a19..ed43e442f 100644 --- a/pkgs/clan-cli/clan_lib/tests/test_create.py +++ b/pkgs/clan-cli/clan_lib/tests/test_create.py @@ -137,7 +137,7 @@ def test_clan_create_api( # TODO: We need to generate a lock file for the templates clan_cli.clan.create.create_clan( clan_cli.clan.create.CreateOptions( - template_name="minimal", dest=dest_clan_dir, update_clan=False + template="minimal", dest=dest_clan_dir, update_clan=False ) ) assert dest_clan_dir.is_dir()