1
0
Fork 0
mirror of https://github.com/LnL7/nix-darwin.git synced 2024-12-14 11:57:34 +00:00

system.file: init

This commit is contained in:
Sam 2024-11-18 13:19:37 -08:00
parent 55d07816a0
commit 6f81009e9e
No known key found for this signature in database
GPG key ID: 07C4B9795517E3B4
6 changed files with 269 additions and 0 deletions

View file

@ -36,6 +36,7 @@
./system/defaults/ActivityMonitor.nix
./system/defaults/WindowManager.nix
./system/etc.nix
./system/files
./system/keyboard.nix
./system/launchd.nix
./system/nvram.nix

View file

@ -55,6 +55,7 @@ in
# We run `etcChecks` again just in case someone runs `activate`
# directly without `activate-user`.
${cfg.activationScripts.etcChecks.text}
${cfg.activationScripts.filesChecks.text}
${cfg.activationScripts.extraActivation.text}
${cfg.activationScripts.groups.text}
${cfg.activationScripts.users.text}
@ -71,6 +72,7 @@ in
${cfg.activationScripts.keyboard.text}
${cfg.activationScripts.fonts.text}
${cfg.activationScripts.nvram.text}
${cfg.activationScripts.files.text}
${cfg.activationScripts.postActivation.text}

View file

@ -114,6 +114,7 @@ in
ln -s ${cfg.build.patches}/patches $out/patches
ln -s ${cfg.build.etc}/etc $out/etc
ln -s ${cfg.build.files} $out/links.json
ln -s ${cfg.path} $out/sw
mkdir -p $out/Library

View file

@ -0,0 +1,63 @@
{ config, lib, pkgs, ... }:
with lib;
let
text = import ./write-text.nix {
inherit lib;
mkTextDerivation = name: text: pkgs.writeText "system-file-${name}" text;
};
rawFiles = filterAttrs (n: v: v.enable) config.system.file;
files = mapAttrs' (name: value: nameValuePair value.target {
type = "link";
inherit (value) source;
}) rawFiles;
linksJSON = pkgs.writeText "system-files.json" (builtins.toJSON {
version = 1;
inherit files;
});
emptyJSON = pkgs.writeText "empty-files.json" (builtins.toJSON {
version = 1;
files = {};
});
python = lib.getExe pkgs.python3;
linker = ./linker.py;
in
{
options = {
system.file = mkOption {
type = types.attrsOf (types.submodule text);
default = {};
description = ''
Set of files that have to be linked/copied out of the Nix store.
'';
};
};
config = {
system.build.files = linksJSON;
system.activationScripts.filesChecks.text = ''
OLD=/run/current-system/links.json
if [ ! -e "$OLD" ]; then
OLD=${emptyJSON}
fi
CHECK_ONLY=1 ${python} ${linker} "$OLD" "$systemConfig"/links.json
'';
system.activationScripts.files.text = ''
OLD=/run/current-system/links.json
if [ ! -e "$OLD" ]; then
OLD=${emptyJSON}
fi
${python} ${linker} "$OLD" "$systemConfig"/links.json
'';
};
}

View file

@ -0,0 +1,150 @@
from collections import namedtuple
from sys import argv
import json
import os
import shutil
import tempfile
if not len(argv) == 3:
print(f"Usage: {argv[0]} <old_system_links.json> <new_system_links.json>")
exit(1)
with open(argv[1], "r") as file:
old_files = json.load(file)
with open(argv[2], "r") as file:
new_files = json.load(file)
if new_files['version'] != 1:
print("Unknown schema version")
exit(1)
DRY_RUN = 'DRY_RUN' in os.environ.keys()
CHECK_ONLY = 'CHECK_ONLY' in os.environ.keys()
Transaction = namedtuple("Transaction", ["source", "destination", "type"])
transactions: list[Transaction] = []
problems: list[str] = []
# Go through all files in the new generation
path: str
for path in new_files['files']:
new_file = new_files['files'][path]
if os.path.lexists(path):
# There is a file at this path
# It could be a regular file or a symlink (including broken symlinks)
if os.path.islink(path):
# The file is a symlink
if path in old_files['files']:
# The old generation had a file at this path
if old_files['files'][path]['type'] == "link":
# The old generation's file was a link
link_target = os.readlink(path)
if os.path.join(os.path.dirname(path), link_target) == old_files['files'][path]['source']:
# The link has not changed since last system activation, so we can overwrite it
transactions.append(Transaction(new_file['source'], path, 'link'))
elif os.path.join(os.path.dirname(path), link_target) == new_file['source']:
# The link already points to the new target
continue
else:
# The link is to somewhere else
problems.append(path)
else:
# The old generation's file was not a link.
# Because we know that the file on disk is a link,
# we know that we can't overwrite this file
problems.append(path)
else:
# The old generation did not have a file at this path,
# and we never overwrite links that weren't created by us
problems.append(path)
else:
# The file is a regular file
problems.append(path)
else:
# There is no file at this path
transactions.append(Transaction(new_file['source'], path, new_file['type']))
# Check problems
for problem in problems:
print(f"Existing file at path {problem}")
if len(problems) > 0:
print("Aborting")
exit(1)
if CHECK_ONLY:
# We don't perform any checks when planning removal of old files, so we can exit here
exit(0)
# Remove all remaining files from the old generation that aren't in the new generation
path: str
for path in old_files['files']:
old_file = old_files['files'][path]
if path in new_files['files']:
# Already handled when we iterated through new_files above
continue
if os.path.lexists(path):
# There is a file at this path
# It could be a regular file or a symlink (including broken symlinks)
if os.path.islink(path):
# The file is a symlink
if old_file['type'] != "link":
# This files wasn't a link at last activation, which means that the user changed it
# Therefore we don't touch it
continue
# Check that its destination remains the same
link_target = os.readlink(path)
if os.path.join(os.path.dirname(path), link_target) == old_file['source']:
# The link has not changed since last system activation, so we can overwrite it
transactions.append(Transaction(path, path, "remove"))
else:
# The link is to somewhere else, so leave it alone
continue
else:
# The file is a regular file
continue
else:
# There's no file at this path anymore, so we have nothing to do anyway
continue
# Perform all transactions
for t in transactions:
# NOTE: the naming scheme for transaction properties is confusing
# We are **NOT** using the same scheme as symlinks when we talk about
# source/destination. The way we are using these names, `source` is a path
# in the Nix store, and `destination` is the path in the system where the source
# should be linked or copied to.
# In the special case of removing files, `destination` can be ignored
if DRY_RUN:
match t.type:
case "link":
print(f"ln -s {t.source} {t.destination}")
case "remove":
print(f"rm {t.source}")
case _:
print(f"Unknown transaction type {t.type}")
else:
match t.type:
case "link":
# TODO: ensure enclosing directory exists
# https://stackoverflow.com/a/55742015/8387516
dir = os.path.dirname(t.destination)
while True:
temp_name = tempfile.mktemp(dir=dir)
try:
os.symlink(t.source, temp_name)
break
except FileExistsError:
pass
os.replace(temp_name, t.destination)
case "remove":
os.remove(t.source)
case _:
print(f"Unknown transaction type {t.type}")

View file

@ -0,0 +1,52 @@
{ lib, mkTextDerivation }:
{ config, name, ... }:
with lib;
let
fileName = file: last (splitString "/" file);
mkDefaultIf = cond: value: mkIf cond (mkDefault value);
drv = mkTextDerivation (fileName name) config.text;
in
{
options = {
enable = mkOption {
type = types.bool;
default = true;
description = ''
Whether this file should be generated.
This option allows specific files to be disabled.
'';
};
text = mkOption {
type = types.lines;
default = "";
description = ''
Text of the file.
'';
};
target = mkOption {
type = types.str;
default = "/${name}";
description = ''
Name of symlink. Defaults to the attribute name preceded by a slash (the root directory).
'';
};
source = mkOption {
type = types.path;
description = ''
Path of the source file.
'';
};
};
config = {
source = mkDefault drv;
};
}