Support tweaking the shell in config

This commit is contained in:
Reid 'arrdem' McKenzie 2022-11-24 16:13:03 -07:00
parent 0dcb4118e2
commit 82d3b0c97c
4 changed files with 40 additions and 31 deletions

View file

@ -99,30 +99,30 @@ def handle_requires(_ctx, _params, requires):
return expand_requires(requires) return expand_requires(requires)
def load(root: Path, name: str, clss): def load(config, root: Path, name: str, clss):
for c in clss: for c in clss:
i = c(root, name) i = c(config, root, name)
if i.test(): if i.test():
return i return i
def load_package(root, name): def load_package(config, root, name):
log.debug(f"Attempting to load package {name} from {root}") log.debug(f"Attempting to load package {name} from {root}")
return load(root, name, [LightPackageV1, PackageV1, PackageV0]) return load(config, root, name, [LightPackageV1, PackageV1, PackageV0])
def load_profile(root, name): def load_profile(config, root, name):
log.debug(f"Attempting to load profile {name} from {root}") log.debug(f"Attempting to load profile {name} from {root}")
return load(root, name, [ProfileV1, ProfileV0]) return load(config, root, name, [ProfileV1, ProfileV0])
def load_packages(root: Path, roots) -> dict: def load_packages(config, root: Path, roots) -> dict:
"""Load the configured packages.""" """Load the configured packages."""
def _load_package(p): def _load_package(p):
name = str(p.relative_to(root)).replace(".toml", "") name = str(p.relative_to(root)).replace(".toml", "")
try: try:
packages[name] = load_package(p, name) packages[name] = load_package(config, p, name)
except Exception as e: except Exception as e:
log.exception(f"Unable to load {p} due to exception") log.exception(f"Unable to load {p} due to exception")
@ -146,7 +146,7 @@ def load_packages(root: Path, roots) -> dict:
# Register the metapackages themselves using the profile type # Register the metapackages themselves using the profile type
mp_name = str(mp_root.relative_to(root)) mp_name = str(mp_root.relative_to(root))
packages[mp_name] = load_profile(mp_root, mp_name) packages[mp_name] = load_profile(config, mp_root, mp_name)
log.debug(f"Loaded {len(packages)} packages...") log.debug(f"Loaded {len(packages)} packages...")
return packages return packages
@ -166,11 +166,11 @@ def missing_require_ignore(r, e):
def build_fs( def build_fs(
root: Path, roots, dest: Path, prelude: List[str], missing_require_handler config, root: Path, roots, dest: Path, prelude: List[str], missing_require_handler
) -> Vfs: ) -> Vfs:
"""Build a VFS by configuring dest from the given config root.""" """Build a VFS by configuring dest from the given config root."""
packages = load_packages(root, roots) packages = load_packages(config, root, roots)
if not packages: if not packages:
log.warning("Loaded no packages!") log.warning("Loaded no packages!")
@ -276,7 +276,7 @@ def configure(ctx, param, filename: Optional[Path]):
if filename and filename.exists(): if filename and filename.exists():
ctx.default_map = {} ctx.default_map = {}
log.debug(f"Loading config from {filename}") log.debug(f"Loading config from {filename}")
cfg = toml.load(filename) ctx.obj = cfg = toml.load(filename)
assert cfg["cram"]["version"] >= 1 assert cfg["cram"]["version"] >= 1
task_cfg = cfg.get("cram", {}).get("task", {}) task_cfg = cfg.get("cram", {}).get("task", {})
@ -349,6 +349,7 @@ Features
- 0.2.0 initial release - 0.2.0 initial release
- 0.2.1 TOML root config - 0.2.1 TOML root config
- 0.2.3 Single-file packages (ex. packages.d/foo.toml) - 0.2.3 Single-file packages (ex. packages.d/foo.toml)
- 0.2.4 Support tweaking the shell
About About
{__copyright__}, {__author__}. {__copyright__}, {__author__}.
@ -410,6 +411,7 @@ def arg_optimize(*args, **kwargs):
@cli.command("apply") @cli.command("apply")
@click.pass_obj
@click.option("--execute/--dry-run", default=False) @click.option("--execute/--dry-run", default=False)
@click.option("--force/--no-force", default=False) @click.option("--force/--no-force", default=False)
@click.option("--exec-idempotent/--exec-always", "exec_idempotent", default=True) @click.option("--exec-idempotent/--exec-always", "exec_idempotent", default=True)
@ -421,6 +423,7 @@ def arg_optimize(*args, **kwargs):
@click.argument("confdir", type=Path, callback=handle_path_vars) @click.argument("confdir", type=Path, callback=handle_path_vars)
@click.argument("destdir", type=Path, callback=handle_path_vars) @click.argument("destdir", type=Path, callback=handle_path_vars)
def do_apply( def do_apply(
obj,
confdir, confdir,
destdir, destdir,
state_file, state_file,
@ -453,7 +456,7 @@ def do_apply(
# Force an empty state # Force an empty state
old_fs = Vfs([]) old_fs = Vfs([])
new_fs = build_fs(root, require_root, dest, require, missing_require) new_fs = build_fs(obj, root, require_root, dest, require, missing_require)
log.debug(f"Built new state consisting of {len(new_fs._log)} steps") log.debug(f"Built new state consisting of {len(new_fs._log)} steps")
# Middleware processing of the resulting filesystem(s) # Middleware processing of the resulting filesystem(s)
@ -478,13 +481,14 @@ def do_apply(
@cli.command("list") @cli.command("list")
@click.pass_obj
@click.option( @click.option(
"-1", "--oneline", is_flag=True, default=False, help="Only list names of resources" "-1", "--oneline", is_flag=True, default=False, help="Only list names of resources"
) )
@arg_require_root @arg_require_root
@click.argument("confdir", type=Path, callback=handle_path_vars) @click.argument("confdir", type=Path, callback=handle_path_vars)
@click.argument("requirements", nargs=-1, callback=handle_requires) @click.argument("requirements", nargs=-1, callback=handle_requires)
def do_list(confdir, requirements, require_root, oneline): def do_list(obj, confdir, requirements, require_root, oneline):
"""List out packages, profiles, hosts and subpackages in the <confdir>.""" """List out packages, profiles, hosts and subpackages in the <confdir>."""
root = confdir.resolve().absolute() root = confdir.resolve().absolute()
@ -495,7 +499,7 @@ def do_list(confdir, requirements, require_root, oneline):
log.fatal(f"{confdir} does not exist!") log.fatal(f"{confdir} does not exist!")
_exit(1) _exit(1)
packages = load_packages(root, require_root) packages = load_packages(obj, root, require_root)
if requirements: if requirements:
dest = Path("~/") dest = Path("~/")
@ -544,10 +548,11 @@ def do_state(confdir, state_file):
@cli.command("fmt") @cli.command("fmt")
@click.pass_obj
@arg_require_root @arg_require_root
@click.argument("confdir", type=Path, callback=handle_path_vars) @click.argument("confdir", type=Path, callback=handle_path_vars)
@click.argument("requirement", type=str, callback=handle_path_vars) @click.argument("requirement", type=str, callback=handle_path_vars)
def do_fmt(confdir: Path, requirement, require_root): def do_fmt(obj, confdir: Path, requirement, require_root):
"""Format the specified requirement to a canonical-ish representation.""" """Format the specified requirement to a canonical-ish representation."""
root = confdir.resolve().absolute() root = confdir.resolve().absolute()
@ -557,7 +562,7 @@ def do_fmt(confdir: Path, requirement, require_root):
log.fatal(f"{confdir} does not exist!") log.fatal(f"{confdir} does not exist!")
_exit(1) _exit(1)
packages = load_packages(root, require_root) packages = load_packages(obj, root, require_root)
pkg = packages[requirement] pkg = packages[requirement]
json = pkg.json() json = pkg.json()
@ -606,6 +611,7 @@ require_root = [
missing_require = "error" missing_require = "error"
[cram.task.apply] [cram.task.apply]
shell = ["/bin/sh"]
missing_require = "warn" missing_require = "warn"
require = [ require = [
"hosts.d/${FQDN}", "hosts.d/${FQDN}",

View file

@ -8,17 +8,19 @@ from typing import List, Optional
from vfs import Vfs from vfs import Vfs
# FIXME: This should be a config somewhere SHELL = ["/bin/sh"]
SHELL = "/bin/sh"
# Light monkeypatching because macos ships a "stable" a py # Light monkeypatching because macos ships a "stable" a py
if sys.version_info <= (3, 9, 0): if sys.version_info <= (3, 9, 0):
Path.readlink = lambda p: Path(os.readlink(str(p))) Path.readlink = lambda p: Path(os.readlink(str(p)))
def sh(cmd: List[str], /, def sh(global_config,
cmd: List[str], /,
env: Optional[dict] = None): env: Optional[dict] = None):
shell = (global_config or {}).get("cram", {}).get("task", {}).get("apply", {}).get("shell", SHELL)
prefix = [] prefix = []
if env: if env:
prefix.append("/usr/bin/env") prefix.append("/usr/bin/env")
@ -26,7 +28,7 @@ def sh(cmd: List[str], /,
v = sh_quote(str(v)) v = sh_quote(str(v))
prefix.append(f"{k}={v}") prefix.append(f"{k}={v}")
return tuple(prefix + [SHELL, *cmd]) return tuple(prefix + [*shell, *cmd])
def stow(fs: Vfs, src_dir: Path, dest_dir: Path, skip=[]): def stow(fs: Vfs, src_dir: Path, dest_dir: Path, skip=[]):
@ -56,7 +58,8 @@ def stow(fs: Vfs, src_dir: Path, dest_dir: Path, skip=[]):
class Package(object): class Package(object):
def __init__(self, root: Path, name: str): def __init__(self, global_config, root: Path, name: str):
self.global_config = global_config
self.root = root self.root = root
self.name = name self.name = name

View file

@ -36,21 +36,21 @@ class PackageV0(Package):
"""Install this package.""" """Install this package."""
buildf = self.root / "BUILD" buildf = self.root / "BUILD"
if buildf.exists(): if buildf.exists():
fs.exec(self.root, sh([str(buildf)])) fs.exec(self.root, sh(self.global_config, [str(buildf)]))
pref = self.root / "PRE_INSTALL" pref = self.root / "PRE_INSTALL"
if pref.exists(): if pref.exists():
fs.exec(self.root, sh([str(pref)])) fs.exec(self.root, sh(self.global_config, [str(pref)]))
installf = self.root / "INSTALL" installf = self.root / "INSTALL"
if installf.exists(): if installf.exists():
fs.exec(self.root, sh([str(installf)])) fs.exec(self.root, sh(self.global_config, [str(installf)]))
else: else:
stow(fs, self.root, dest, self.SPECIAL_FILES) stow(fs, self.root, dest, self.SPECIAL_FILES)
postf = self.root / "POST_INSTALL" postf = self.root / "POST_INSTALL"
if postf.exists(): if postf.exists():
fs.exec(self.root, sh([str(postf)])) fs.exec(self.root, sh(self.global_config, [str(postf)]))
def _read(self, p: Path): def _read(self, p: Path):
if p.exists(): if p.exists():
@ -94,16 +94,16 @@ class ProfileV0(PackageV0):
buildf = self.root / "BUILD" buildf = self.root / "BUILD"
if buildf.exists(): if buildf.exists():
fs.exec(self.root, sh([str(buildf)])) fs.exec(self.root, sh(self.global_config, [str(buildf)]))
pref = self.root / "PRE_INSTALL" pref = self.root / "PRE_INSTALL"
if pref.exists(): if pref.exists():
fs.exec(self.root, sh([str(pref)])) fs.exec(self.root, sh(self.global_config, [str(pref)]))
installf = self.root / "INSTALL" installf = self.root / "INSTALL"
if installf.exists(): if installf.exists():
fs.exec(self.root, sh([str(installf)])) fs.exec(self.root, sh(self.global_config, [str(installf)]))
postf = self.root / "POST_INSTALL" postf = self.root / "POST_INSTALL"
if postf.exists(): if postf.exists():
fs.exec(self.root, sh([str(postf)])) fs.exec(self.root, sh(self.global_config, [str(postf)]))

View file

@ -79,7 +79,7 @@ class PackageV1(Package):
f = tempf(f"{sum}.sh") f = tempf(f"{sum}.sh")
with open(f, "w") as fp: with open(f, "w") as fp:
fp.write(content) fp.write(content)
fs.exec(cwd, sh([f])) fs.exec(cwd, sh(self.global_config, [f]))
def do_build(self, fs: Vfs, dest: Path): def do_build(self, fs: Vfs, dest: Path):
self.do_sh_or_script(self.config().get("package", {}).get("build"), fs, dest) self.do_sh_or_script(self.config().get("package", {}).get("build"), fs, dest)