added env var support
This commit is contained in:
parent
7d51de1052
commit
25c80fdf3b
3 changed files with 390 additions and 10 deletions
33
README.md
33
README.md
|
|
@ -66,8 +66,9 @@ This command:
|
||||||
2. Disables COPR repositories that are no longer configured
|
2. Disables COPR repositories that are no longer configured
|
||||||
3. Installs all packages listed in `inventory.toml`
|
3. Installs all packages listed in `inventory.toml`
|
||||||
4. Creates symlinks for all configured dotfiles
|
4. Creates symlinks for all configured dotfiles
|
||||||
5. Creates backups of existing files before replacing them
|
5. Applies environment variables to `~/.config/environment.d/20-curator.conf` and imports them into the user session
|
||||||
6. Updates the last switch timestamp
|
6. Creates backups of existing files before replacing them
|
||||||
|
7. Updates the last switch timestamp
|
||||||
|
|
||||||
### `status`
|
### `status`
|
||||||
Show current configuration status and information.
|
Show current configuration status and information.
|
||||||
|
|
@ -117,6 +118,15 @@ Remove the rollback snapshot (`inventory.toml.rollback`).
|
||||||
curator reset
|
curator reset
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `env`
|
||||||
|
Apply environment variables defined in `[variables]` and propagate them to systemd/DBus. For your current shell, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curator env
|
||||||
|
# then, to update this shell:
|
||||||
|
eval "$(curator env --eval)"
|
||||||
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
### inventory.toml
|
### inventory.toml
|
||||||
|
|
@ -156,6 +166,10 @@ last_switch = ""
|
||||||
# nixpkgs#git # or just "git" (curator will prefix nixpkgs#)
|
# nixpkgs#git # or just "git" (curator will prefix nixpkgs#)
|
||||||
# nixpkgs#htop # or just "htop"
|
# nixpkgs#htop # or just "htop"
|
||||||
|
|
||||||
|
[variables]
|
||||||
|
# ENV_VAR = "value"
|
||||||
|
# ANOTHER = "another value"
|
||||||
|
|
||||||
[dotfiles]
|
[dotfiles]
|
||||||
# Dotfiles to manage with symlinks
|
# Dotfiles to manage with symlinks
|
||||||
# Format: "target_path" = "source_path"
|
# Format: "target_path" = "source_path"
|
||||||
|
|
@ -217,6 +231,21 @@ List of rpm-ostree layered packages. **One package per line**—presence means i
|
||||||
#### `[nix]`
|
#### `[nix]`
|
||||||
List of nix packages. **One package per line**—presence means install. You can specify plain names (e.g., `neovim`) or `nixpkgs#name`; curator will prefix `nixpkgs#` for installs and use the base name for removals.
|
List of nix packages. **One package per line**—presence means install. You can specify plain names (e.g., `neovim`) or `nixpkgs#name`; curator will prefix `nixpkgs#` for installs and use the base name for removals.
|
||||||
|
|
||||||
|
#### `[variables]`
|
||||||
|
User environment variables to set via systemd `environment.d`. curator writes them to `~/.config/environment.d/20-curator.conf` during `curator switch`; new sessions will load them automatically.
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```toml
|
||||||
|
[variables]
|
||||||
|
EDITOR = "nvim"
|
||||||
|
PAGER = "less -R"
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Values are imported into the user systemd and DBus environments during `curator switch` or `curator env`.
|
||||||
|
- To update a currently running shell, run `eval "$(curator env --eval)"` after applying.
|
||||||
|
- Later shell init files (e.g., `.bashrc`, `.zshrc`) can still override these values.
|
||||||
|
|
||||||
#### `[dotfiles]`
|
#### `[dotfiles]`
|
||||||
Dotfile mappings using symlinks:
|
Dotfile mappings using symlinks:
|
||||||
- **Key**: Target path where symlink should be created (relative to home directory)
|
- **Key**: Target path where symlink should be created (relative to home directory)
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,9 @@ last_switch = ""
|
||||||
# nixpkgs#git
|
# nixpkgs#git
|
||||||
# nixpkgs#htop
|
# nixpkgs#htop
|
||||||
|
|
||||||
|
[variables]
|
||||||
|
# EXAMPLE_VAR = "value"
|
||||||
|
|
||||||
[dotfiles]
|
[dotfiles]
|
||||||
# Dotfiles to manage with symlinks
|
# Dotfiles to manage with symlinks
|
||||||
# Format: "target_path" = "source_path"
|
# Format: "target_path" = "source_path"
|
||||||
|
|
@ -77,6 +80,7 @@ class CuratorConfig:
|
||||||
flatpak_refs: list[str]
|
flatpak_refs: list[str]
|
||||||
rpm_ostree_packages: list[str]
|
rpm_ostree_packages: list[str]
|
||||||
nix_packages: list[str]
|
nix_packages: list[str]
|
||||||
|
variables: dict[str, str]
|
||||||
dotfiles: dict[str, str]
|
dotfiles: dict[str, str]
|
||||||
options: dict[str, Any]
|
options: dict[str, Any]
|
||||||
curator_fields: dict[str, Any]
|
curator_fields: dict[str, Any]
|
||||||
|
|
@ -118,6 +122,17 @@ def parse_value(value: str) -> Any:
|
||||||
return strip_quotes(value)
|
return strip_quotes(value)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_key_value(entry: str) -> Optional[tuple[str, str]]:
|
||||||
|
if "=" not in entry:
|
||||||
|
return None
|
||||||
|
key_raw, value_raw = entry.split("=", 1)
|
||||||
|
key = strip_quotes(key_raw.strip())
|
||||||
|
value = strip_quotes(value_raw.strip())
|
||||||
|
if not key:
|
||||||
|
return None
|
||||||
|
return key, value
|
||||||
|
|
||||||
|
|
||||||
def normalize_nix_ref(ref: str) -> str:
|
def normalize_nix_ref(ref: str) -> str:
|
||||||
ref = ref.strip()
|
ref = ref.strip()
|
||||||
if not ref:
|
if not ref:
|
||||||
|
|
@ -146,6 +161,7 @@ def parse_config(config_path: Path) -> CuratorConfig:
|
||||||
flatpak_refs: list[str] = []
|
flatpak_refs: list[str] = []
|
||||||
rpm_ostree_packages: list[str] = []
|
rpm_ostree_packages: list[str] = []
|
||||||
nix_packages: list[str] = []
|
nix_packages: list[str] = []
|
||||||
|
variables: dict[str, str] = {}
|
||||||
dotfiles: dict[str, str] = {}
|
dotfiles: dict[str, str] = {}
|
||||||
options: dict[str, Any] = {}
|
options: dict[str, Any] = {}
|
||||||
curator_fields: dict[str, Any] = {}
|
curator_fields: dict[str, Any] = {}
|
||||||
|
|
@ -182,6 +198,16 @@ def parse_config(config_path: Path) -> CuratorConfig:
|
||||||
dnf_packages.append(strip_quotes(entry))
|
dnf_packages.append(strip_quotes(entry))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if section == "variables":
|
||||||
|
if "=" not in line:
|
||||||
|
continue
|
||||||
|
key_raw, value_raw = line.split("=", 1)
|
||||||
|
key = strip_quotes(key_raw.strip())
|
||||||
|
value = strip_quotes(value_raw.strip())
|
||||||
|
if key:
|
||||||
|
variables[key] = value
|
||||||
|
continue
|
||||||
|
|
||||||
if section == "dotfiles":
|
if section == "dotfiles":
|
||||||
if "=" not in line:
|
if "=" not in line:
|
||||||
continue
|
continue
|
||||||
|
|
@ -219,6 +245,7 @@ def parse_config(config_path: Path) -> CuratorConfig:
|
||||||
flatpak_refs=flatpak_refs,
|
flatpak_refs=flatpak_refs,
|
||||||
rpm_ostree_packages=rpm_ostree_packages,
|
rpm_ostree_packages=rpm_ostree_packages,
|
||||||
nix_packages=nix_packages,
|
nix_packages=nix_packages,
|
||||||
|
variables=variables,
|
||||||
dotfiles=dotfiles,
|
dotfiles=dotfiles,
|
||||||
options=merged_options,
|
options=merged_options,
|
||||||
curator_fields=curator_fields,
|
curator_fields=curator_fields,
|
||||||
|
|
@ -234,6 +261,7 @@ def empty_config() -> CuratorConfig:
|
||||||
flatpak_refs=[],
|
flatpak_refs=[],
|
||||||
rpm_ostree_packages=[],
|
rpm_ostree_packages=[],
|
||||||
nix_packages=[],
|
nix_packages=[],
|
||||||
|
variables={},
|
||||||
dotfiles={},
|
dotfiles={},
|
||||||
options={"backup": True, "backup_dir": "backup"},
|
options={"backup": True, "backup_dir": "backup"},
|
||||||
curator_fields={},
|
curator_fields={},
|
||||||
|
|
@ -609,7 +637,7 @@ def gather_nix_packages() -> list[str]:
|
||||||
return sorted(collected)
|
return sorted(collected)
|
||||||
|
|
||||||
|
|
||||||
def backup_target(target_path: Path, backup_dir: Path) -> None:
|
def backup_target(target_path: Path, backup_dir: Path, *, quiet: bool = False) -> None:
|
||||||
timestamp = dt.datetime.now().strftime("%Y%m%d_%H%M%S")
|
timestamp = dt.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||||
backup_path = backup_dir / f"{target_path.name}.{timestamp}.bak"
|
backup_path = backup_dir / f"{target_path.name}.{timestamp}.bak"
|
||||||
|
|
@ -619,6 +647,7 @@ def backup_target(target_path: Path, backup_dir: Path) -> None:
|
||||||
else:
|
else:
|
||||||
shutil.copy2(target_path, backup_path)
|
shutil.copy2(target_path, backup_path)
|
||||||
|
|
||||||
|
if not quiet:
|
||||||
log_info(f"Backed up {target_path} to {backup_path}")
|
log_info(f"Backed up {target_path} to {backup_path}")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -671,6 +700,162 @@ def deploy_dotfiles(config: CuratorConfig, curator_dir: Path) -> None:
|
||||||
log_error(f"Failed to create symlink: {target_path} -> {relative_source} ({exc})")
|
log_error(f"Failed to create symlink: {target_path} -> {relative_source} ({exc})")
|
||||||
|
|
||||||
|
|
||||||
|
def _format_env_value(value: str) -> str:
|
||||||
|
sanitized = value.replace("\r", " ").replace("\n", " ")
|
||||||
|
escaped = sanitized.replace("\\", "\\\\").replace('"', '\\"')
|
||||||
|
if re.search(r"\s|#", sanitized) or '"' in sanitized:
|
||||||
|
return f'"{escaped}"'
|
||||||
|
return escaped
|
||||||
|
|
||||||
|
|
||||||
|
def _read_environment_file_keys(env_file: Path) -> set[str]:
|
||||||
|
if not env_file.exists():
|
||||||
|
return set()
|
||||||
|
keys: set[str] = set()
|
||||||
|
try:
|
||||||
|
for line in env_file.read_text().splitlines():
|
||||||
|
match = re.match(r"\s*([A-Za-z_][A-Za-z0-9_]*)\s*=", line)
|
||||||
|
if match:
|
||||||
|
keys.add(match.group(1))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return keys
|
||||||
|
|
||||||
|
|
||||||
|
def _run_env_command(command: list[str], env: dict[str, str], quiet: bool, description: str) -> None:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(command, check=False, env=env, capture_output=True, text=True)
|
||||||
|
except FileNotFoundError:
|
||||||
|
if not quiet:
|
||||||
|
log_warning(f"{description} skipped (command not found): {command[0]}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
if not quiet:
|
||||||
|
details = result.stderr.strip() or result.stdout.strip() or "unknown error"
|
||||||
|
log_warning(f"{description} failed: {details}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not quiet:
|
||||||
|
log_success(f"{description} complete")
|
||||||
|
|
||||||
|
|
||||||
|
def propagate_environment_variables(vars_map: dict[str, str], unset_keys: set[str], quiet: bool = False) -> None:
|
||||||
|
if not vars_map and not unset_keys:
|
||||||
|
return
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
env.update(vars_map)
|
||||||
|
|
||||||
|
set_keys = sorted(vars_map)
|
||||||
|
unset_list = sorted(unset_keys)
|
||||||
|
|
||||||
|
if set_keys:
|
||||||
|
_run_env_command(
|
||||||
|
["systemctl", "--user", "import-environment", *set_keys],
|
||||||
|
env,
|
||||||
|
quiet,
|
||||||
|
"Imported environment into systemd --user",
|
||||||
|
)
|
||||||
|
|
||||||
|
_run_env_command(
|
||||||
|
["dbus-update-activation-environment", "--systemd", *[f"{k}={v}" for k, v in vars_map.items()]],
|
||||||
|
env,
|
||||||
|
quiet,
|
||||||
|
"Updated DBus activation environment",
|
||||||
|
)
|
||||||
|
|
||||||
|
if unset_list:
|
||||||
|
_run_env_command(
|
||||||
|
["systemctl", "--user", "unset-environment", *unset_list],
|
||||||
|
env,
|
||||||
|
quiet,
|
||||||
|
"Unset environment in systemd --user",
|
||||||
|
)
|
||||||
|
_run_env_command(
|
||||||
|
["dbus-update-activation-environment", "--systemd", *[f"--unset={key}" for key in unset_list]],
|
||||||
|
env,
|
||||||
|
quiet,
|
||||||
|
"Unset DBus activation environment",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_environment_variables(
|
||||||
|
config: CuratorConfig, curator_dir: Path, *, quiet: bool = False, propagate: bool = False
|
||||||
|
) -> tuple[dict[str, str], set[str]]:
|
||||||
|
def log_info_if(message: str) -> None:
|
||||||
|
if not quiet:
|
||||||
|
log_info(message)
|
||||||
|
|
||||||
|
def log_success_if(message: str) -> None:
|
||||||
|
if not quiet:
|
||||||
|
log_success(message)
|
||||||
|
|
||||||
|
def log_warning_if(message: str) -> None:
|
||||||
|
if not quiet:
|
||||||
|
log_warning(message)
|
||||||
|
|
||||||
|
log_info_if("Applying environment variables...")
|
||||||
|
env_home = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")).expanduser()
|
||||||
|
env_dir = env_home / "environment.d"
|
||||||
|
env_file = env_dir / "20-curator.conf"
|
||||||
|
|
||||||
|
backup_enabled = bool(config.options.get("backup", True))
|
||||||
|
backup_dir_name = str(config.options.get("backup_dir", "backup") or "backup")
|
||||||
|
backup_dir = curator_dir / backup_dir_name
|
||||||
|
|
||||||
|
existing_keys = _read_environment_file_keys(env_file)
|
||||||
|
|
||||||
|
variables: dict[str, str] = {}
|
||||||
|
for key, raw_value in config.variables.items():
|
||||||
|
key_clean = key.strip()
|
||||||
|
if not key_clean:
|
||||||
|
log_warning_if("Skipping environment variable with empty name")
|
||||||
|
continue
|
||||||
|
variables[key_clean] = str(raw_value)
|
||||||
|
|
||||||
|
removed_keys = existing_keys - set(variables.keys())
|
||||||
|
|
||||||
|
if not variables:
|
||||||
|
if env_file.exists():
|
||||||
|
try:
|
||||||
|
if backup_enabled:
|
||||||
|
backup_target(env_file, backup_dir, quiet=quiet)
|
||||||
|
env_file.unlink()
|
||||||
|
log_success_if(f"Removed environment file: {env_file}")
|
||||||
|
except OSError as exc:
|
||||||
|
log_warning_if(f"Failed to remove environment file {env_file}: {exc}")
|
||||||
|
else:
|
||||||
|
log_info_if("No environment variables to apply")
|
||||||
|
if propagate and (removed_keys or variables):
|
||||||
|
propagate_environment_variables(variables, removed_keys, quiet)
|
||||||
|
return variables, removed_keys
|
||||||
|
|
||||||
|
lines: list[str] = []
|
||||||
|
for key, raw_value in sorted(variables.items()):
|
||||||
|
formatted_value = _format_env_value(str(raw_value))
|
||||||
|
lines.append(f"{key}={formatted_value}")
|
||||||
|
|
||||||
|
env_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if backup_enabled and env_file.exists():
|
||||||
|
backup_target(env_file, backup_dir, quiet=quiet)
|
||||||
|
env_file.write_text("\n".join(lines) + "\n")
|
||||||
|
log_success_if(f"Wrote {len(lines)} environment variables to {env_file}")
|
||||||
|
except OSError as exc:
|
||||||
|
if quiet:
|
||||||
|
print(f"ERROR: Failed to write environment variables to {env_file}: {exc}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
log_error(f"Failed to write environment variables to {env_file}: {exc}")
|
||||||
|
return variables, removed_keys
|
||||||
|
|
||||||
|
if propagate:
|
||||||
|
propagate_environment_variables(variables, removed_keys, quiet)
|
||||||
|
|
||||||
|
return variables, removed_keys
|
||||||
|
|
||||||
|
|
||||||
def update_last_switch(config_path: Path, options: dict[str, Any]) -> None:
|
def update_last_switch(config_path: Path, options: dict[str, Any]) -> None:
|
||||||
backup_enabled = bool(options.get("backup", True))
|
backup_enabled = bool(options.get("backup", True))
|
||||||
backup_dir_name = str(options.get("backup_dir", "backup") or "backup")
|
backup_dir_name = str(options.get("backup_dir", "backup") or "backup")
|
||||||
|
|
@ -744,6 +929,8 @@ def update_section_entries(config_path: Path, section: str, entries: list[str])
|
||||||
while i < len(lines) and not re.match(r"\s*\[.+]\s*$", lines[i]):
|
while i < len(lines) and not re.match(r"\s*\[.+]\s*$", lines[i]):
|
||||||
i += 1
|
i += 1
|
||||||
output.extend(entries)
|
output.extend(entries)
|
||||||
|
if i < len(lines) and (not output or output[-1].strip()):
|
||||||
|
output.append("")
|
||||||
continue
|
continue
|
||||||
output.append(line)
|
output.append(line)
|
||||||
i += 1
|
i += 1
|
||||||
|
|
@ -777,6 +964,8 @@ def merge_section_entries(config_path: Path, section: str, add: set[str], remove
|
||||||
current_entries.update(current_config.copr)
|
current_entries.update(current_config.copr)
|
||||||
elif section == "dotfiles":
|
elif section == "dotfiles":
|
||||||
current_entries.update(f"{k} = {v}" for k, v in current_config.dotfiles.items())
|
current_entries.update(f"{k} = {v}" for k, v in current_config.dotfiles.items())
|
||||||
|
elif section == "variables":
|
||||||
|
current_entries.update(f"{k} = {v}" for k, v in current_config.variables.items())
|
||||||
else:
|
else:
|
||||||
log_error(f"Unknown section: {section}")
|
log_error(f"Unknown section: {section}")
|
||||||
return
|
return
|
||||||
|
|
@ -785,6 +974,27 @@ def merge_section_entries(config_path: Path, section: str, add: set[str], remove
|
||||||
add_clean = {normalize_nix_ref(item) for item in add_clean}
|
add_clean = {normalize_nix_ref(item) for item in add_clean}
|
||||||
remove_clean = {normalize_nix_ref(item) for item in remove_clean}
|
remove_clean = {normalize_nix_ref(item) for item in remove_clean}
|
||||||
|
|
||||||
|
if section == "variables":
|
||||||
|
current_vars = dict(current_config.variables)
|
||||||
|
|
||||||
|
for item in add_clean:
|
||||||
|
parsed = parse_key_value(item)
|
||||||
|
if not parsed:
|
||||||
|
log_warning(f"Skipping invalid variable entry: {item}")
|
||||||
|
continue
|
||||||
|
key, value = parsed
|
||||||
|
current_vars[key] = value
|
||||||
|
|
||||||
|
for item in remove_clean:
|
||||||
|
parsed = parse_key_value(item)
|
||||||
|
key = parsed[0] if parsed else strip_quotes(item)
|
||||||
|
key = key.strip()
|
||||||
|
if key and key in current_vars:
|
||||||
|
del current_vars[key]
|
||||||
|
|
||||||
|
update_section_entries(config_path, section, [f"{k} = {v}" for k, v in sorted(current_vars.items())])
|
||||||
|
return
|
||||||
|
|
||||||
new_entries = (current_entries | add_clean) - remove_clean
|
new_entries = (current_entries | add_clean) - remove_clean
|
||||||
update_section_entries(config_path, section, sorted(new_entries))
|
update_section_entries(config_path, section, sorted(new_entries))
|
||||||
|
|
||||||
|
|
@ -864,6 +1074,7 @@ def _switch(rollback: bool) -> None:
|
||||||
remove_flatpaks(flatpak_removed, flatpak_installed)
|
remove_flatpaks(flatpak_removed, flatpak_installed)
|
||||||
remove_rpm_ostree_packages(rpm_ostree_removed, rpm_ostree_installed)
|
remove_rpm_ostree_packages(rpm_ostree_removed, rpm_ostree_installed)
|
||||||
remove_nix_packages(nix_removed_base, set(nix_base_name(p) for p in gather_nix_packages()) if need_nix else None)
|
remove_nix_packages(nix_removed_base, set(nix_base_name(p) for p in gather_nix_packages()) if need_nix else None)
|
||||||
|
apply_environment_variables(current_config, curator_dir, propagate=True)
|
||||||
deploy_dotfiles(current_config, curator_dir)
|
deploy_dotfiles(current_config, curator_dir)
|
||||||
update_last_switch(curator_toml, current_config.options)
|
update_last_switch(curator_toml, current_config.options)
|
||||||
if rollback:
|
if rollback:
|
||||||
|
|
@ -920,9 +1131,12 @@ def apply_inline_updates(entry_args: list[str], add: bool) -> None:
|
||||||
if not section or not value:
|
if not section or not value:
|
||||||
log_warning(f"Skipping invalid entry (empty section or value): {arg}")
|
log_warning(f"Skipping invalid entry (empty section or value): {arg}")
|
||||||
continue
|
continue
|
||||||
if section not in {"dnf", "brew", "flatpak", "rpm-ostree", "nix", "copr"}:
|
if section not in {"dnf", "brew", "flatpak", "rpm-ostree", "nix", "copr", "variables"}:
|
||||||
log_warning(f"Unknown section '{section}' in entry: {arg}")
|
log_warning(f"Unknown section '{section}' in entry: {arg}")
|
||||||
continue
|
continue
|
||||||
|
if section == "variables" and add and "=" not in value:
|
||||||
|
log_warning(f"Skipping invalid variable entry (expected NAME=VALUE): {arg}")
|
||||||
|
continue
|
||||||
bucket = changes.setdefault(section, {"add": set(), "remove": set()})
|
bucket = changes.setdefault(section, {"add": set(), "remove": set()})
|
||||||
target = "add" if add else "remove"
|
target = "add" if add else "remove"
|
||||||
bucket[target].add(value)
|
bucket[target].add(value)
|
||||||
|
|
@ -932,6 +1146,31 @@ def apply_inline_updates(entry_args: list[str], add: bool) -> None:
|
||||||
log_success(f"Updated section [{section}] in {curator_toml}")
|
log_success(f"Updated section [{section}] in {curator_toml}")
|
||||||
|
|
||||||
|
|
||||||
|
def env_command(eval_mode: bool = False) -> None:
|
||||||
|
curator_dir, curator_toml, _, _ = get_paths()
|
||||||
|
if not curator_toml.exists():
|
||||||
|
log_error("inventory.toml not found. Run 'curator init' first.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
config = parse_config(curator_toml)
|
||||||
|
variables, removed_keys = apply_environment_variables(
|
||||||
|
config, curator_dir, quiet=eval_mode, propagate=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if eval_mode:
|
||||||
|
exports: list[str] = []
|
||||||
|
for key, value in sorted(variables.items()):
|
||||||
|
exports.append(f"export {key}={_format_env_value(value)}")
|
||||||
|
for key in sorted(removed_keys):
|
||||||
|
exports.append(f"unset {key}")
|
||||||
|
if exports:
|
||||||
|
print("\n".join(exports))
|
||||||
|
return
|
||||||
|
|
||||||
|
log_info("To update your current shell now, run:")
|
||||||
|
print(' eval "$(curator env --eval)"')
|
||||||
|
|
||||||
|
|
||||||
def status_command() -> None:
|
def status_command() -> None:
|
||||||
curator_dir, curator_toml, _, _ = get_paths()
|
curator_dir, curator_toml, _, _ = get_paths()
|
||||||
log_info("curator Status")
|
log_info("curator Status")
|
||||||
|
|
@ -1006,6 +1245,15 @@ def status_command() -> None:
|
||||||
else:
|
else:
|
||||||
print(" No nix packages configured")
|
print(" No nix packages configured")
|
||||||
|
|
||||||
|
print()
|
||||||
|
log_info("Environment Variables:")
|
||||||
|
if config.variables:
|
||||||
|
for key, value in sorted(config.variables.items()):
|
||||||
|
print(f" {key} = {value}")
|
||||||
|
print(f" Total: {len(config.variables)} environment variables")
|
||||||
|
else:
|
||||||
|
print(" No environment variables configured")
|
||||||
|
|
||||||
print()
|
print()
|
||||||
log_info("Dotfiles:")
|
log_info("Dotfiles:")
|
||||||
if config.dotfiles:
|
if config.dotfiles:
|
||||||
|
|
@ -1040,6 +1288,12 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
add_parser.add_argument("entries", nargs="+", help="Entries in the form section:value")
|
add_parser.add_argument("entries", nargs="+", help="Entries in the form section:value")
|
||||||
remove_parser = subparsers.add_parser("remove", help="Remove entries from inventory.toml (e.g. dnf:uv nix:micro)")
|
remove_parser = subparsers.add_parser("remove", help="Remove entries from inventory.toml (e.g. dnf:uv nix:micro)")
|
||||||
remove_parser.add_argument("entries", nargs="+", help="Entries in the form section:value")
|
remove_parser.add_argument("entries", nargs="+", help="Entries in the form section:value")
|
||||||
|
env_parser = subparsers.add_parser("env", help="Apply environment variables immediately")
|
||||||
|
env_parser.add_argument(
|
||||||
|
"--eval",
|
||||||
|
action="store_true",
|
||||||
|
help='Print export/unset statements for the current shell (use with: eval "$(curator env --eval)")',
|
||||||
|
)
|
||||||
subparsers.add_parser("status", help="Show current configuration status")
|
subparsers.add_parser("status", help="Show current configuration status")
|
||||||
subparsers.add_parser("help", help="Show help message")
|
subparsers.add_parser("help", help="Show help message")
|
||||||
return parser
|
return parser
|
||||||
|
|
@ -1061,6 +1315,8 @@ def main(argv: list[str] | None = None) -> None:
|
||||||
apply_inline_updates(args.entries, add=True)
|
apply_inline_updates(args.entries, add=True)
|
||||||
elif args.command == "remove":
|
elif args.command == "remove":
|
||||||
apply_inline_updates(args.entries, add=False)
|
apply_inline_updates(args.entries, add=False)
|
||||||
|
elif args.command == "env":
|
||||||
|
env_command(eval_mode=args.eval)
|
||||||
elif args.command == "status":
|
elif args.command == "status":
|
||||||
status_command()
|
status_command()
|
||||||
elif args.command == "help":
|
elif args.command == "help":
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,9 @@ podman
|
||||||
[nix]
|
[nix]
|
||||||
nixpkgs#git
|
nixpkgs#git
|
||||||
|
|
||||||
|
[variables]
|
||||||
|
EDITOR = "nvim"
|
||||||
|
|
||||||
[copr]
|
[copr]
|
||||||
copr.fedorainfracloud.org/user/repo
|
copr.fedorainfracloud.org/user/repo
|
||||||
|
|
||||||
|
|
@ -90,6 +93,7 @@ copr.fedorainfracloud.org/user/repo
|
||||||
assert config.nix_packages == ["nixpkgs#git"]
|
assert config.nix_packages == ["nixpkgs#git"]
|
||||||
assert config.copr == ["copr.fedorainfracloud.org/user/repo"]
|
assert config.copr == ["copr.fedorainfracloud.org/user/repo"]
|
||||||
assert config.dotfiles == {".bashrc": ".bashrc"}
|
assert config.dotfiles == {".bashrc": ".bashrc"}
|
||||||
|
assert config.variables == {"EDITOR": "nvim"}
|
||||||
|
|
||||||
|
|
||||||
def test_diff_items() -> None:
|
def test_diff_items() -> None:
|
||||||
|
|
@ -162,3 +166,94 @@ wget
|
||||||
"copr.fedorainfracloud.org/another/repo",
|
"copr.fedorainfracloud.org/another/repo",
|
||||||
"copr.fedorainfracloud.org/user/repo",
|
"copr.fedorainfracloud.org/user/repo",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_section_entries_preserves_spacing(tmp_path: Path) -> None:
|
||||||
|
config_path = tmp_path / "inventory.toml"
|
||||||
|
config_path.write_text(
|
||||||
|
"""
|
||||||
|
[dnf]
|
||||||
|
git
|
||||||
|
|
||||||
|
[brew]
|
||||||
|
wget
|
||||||
|
""".strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
cli.merge_section_entries(config_path, "dnf", {"vim"}, set())
|
||||||
|
|
||||||
|
lines = config_path.read_text().splitlines()
|
||||||
|
assert "" in lines, "expected to keep a blank line between sections"
|
||||||
|
dnf_index = lines.index("[dnf]")
|
||||||
|
brew_index = lines.index("[brew]")
|
||||||
|
assert lines[dnf_index + 1 : dnf_index + 3] == ["git", "vim"]
|
||||||
|
assert lines[brew_index - 1] == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_environment_variables(tmp_path: Path, monkeypatch) -> None:
|
||||||
|
curator_dir = tmp_path / "curator"
|
||||||
|
curator_dir.mkdir()
|
||||||
|
env_home = tmp_path / "xdg"
|
||||||
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(env_home))
|
||||||
|
|
||||||
|
config = cli.empty_config()
|
||||||
|
config.variables = {"EDITOR": "nvim", "WITH_SPACE": "some value"}
|
||||||
|
config.options["backup"] = False
|
||||||
|
|
||||||
|
written, removed = cli.apply_environment_variables(config, curator_dir)
|
||||||
|
|
||||||
|
env_file = env_home / "environment.d" / "20-curator.conf"
|
||||||
|
assert env_file.exists()
|
||||||
|
lines = env_file.read_text().splitlines()
|
||||||
|
assert "EDITOR=nvim" in lines
|
||||||
|
assert 'WITH_SPACE="some value"' in lines
|
||||||
|
assert written == {"EDITOR": "nvim", "WITH_SPACE": "some value"}
|
||||||
|
assert removed == set()
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_environment_variables_removes_file_when_empty(tmp_path: Path, monkeypatch) -> None:
|
||||||
|
curator_dir = tmp_path / "curator"
|
||||||
|
curator_dir.mkdir()
|
||||||
|
env_home = tmp_path / "xdg"
|
||||||
|
env_dir = env_home / "environment.d"
|
||||||
|
env_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
env_file = env_dir / "20-curator.conf"
|
||||||
|
env_file.write_text("OLD=1\n")
|
||||||
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(env_home))
|
||||||
|
|
||||||
|
config = cli.empty_config()
|
||||||
|
config.options["backup"] = False
|
||||||
|
|
||||||
|
variables, removed = cli.apply_environment_variables(config, curator_dir)
|
||||||
|
|
||||||
|
assert not env_file.exists()
|
||||||
|
assert variables == {}
|
||||||
|
assert removed == {"OLD"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_env_command_eval_outputs_exports(tmp_path: Path, monkeypatch, capsys) -> None:
|
||||||
|
curator_dir = tmp_path / "curator"
|
||||||
|
curator_dir.mkdir()
|
||||||
|
env_file = curator_dir / "inventory.toml"
|
||||||
|
env_file.write_text(
|
||||||
|
"""
|
||||||
|
[variables]
|
||||||
|
EDITOR = "nvim"
|
||||||
|
OLD = "1"
|
||||||
|
""".strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_get_paths():
|
||||||
|
return curator_dir, env_file, curator_dir / "last", curator_dir / "rollback"
|
||||||
|
|
||||||
|
def fake_apply_environment_variables(config, path, quiet=False, propagate=False):
|
||||||
|
assert quiet is True
|
||||||
|
assert propagate is True
|
||||||
|
return {"EDITOR": "nvim"}, {"OLD"}
|
||||||
|
|
||||||
|
monkeypatch.setattr(cli, "get_paths", fake_get_paths)
|
||||||
|
monkeypatch.setattr(cli, "apply_environment_variables", fake_apply_environment_variables)
|
||||||
|
|
||||||
|
cli.env_command(eval_mode=True)
|
||||||
|
out = capsys.readouterr().out.strip().splitlines()
|
||||||
|
assert out == ["export EDITOR=nvim", "unset OLD"]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue