mirror of
https://github.com/LnL7/nix-darwin.git
synced 2024-12-15 17:51:01 +00:00
system.file: init
This commit is contained in:
parent
55d07816a0
commit
6f81009e9e
6 changed files with 269 additions and 0 deletions
|
@ -36,6 +36,7 @@
|
||||||
./system/defaults/ActivityMonitor.nix
|
./system/defaults/ActivityMonitor.nix
|
||||||
./system/defaults/WindowManager.nix
|
./system/defaults/WindowManager.nix
|
||||||
./system/etc.nix
|
./system/etc.nix
|
||||||
|
./system/files
|
||||||
./system/keyboard.nix
|
./system/keyboard.nix
|
||||||
./system/launchd.nix
|
./system/launchd.nix
|
||||||
./system/nvram.nix
|
./system/nvram.nix
|
||||||
|
|
|
@ -55,6 +55,7 @@ in
|
||||||
# We run `etcChecks` again just in case someone runs `activate`
|
# We run `etcChecks` again just in case someone runs `activate`
|
||||||
# directly without `activate-user`.
|
# directly without `activate-user`.
|
||||||
${cfg.activationScripts.etcChecks.text}
|
${cfg.activationScripts.etcChecks.text}
|
||||||
|
${cfg.activationScripts.filesChecks.text}
|
||||||
${cfg.activationScripts.extraActivation.text}
|
${cfg.activationScripts.extraActivation.text}
|
||||||
${cfg.activationScripts.groups.text}
|
${cfg.activationScripts.groups.text}
|
||||||
${cfg.activationScripts.users.text}
|
${cfg.activationScripts.users.text}
|
||||||
|
@ -71,6 +72,7 @@ in
|
||||||
${cfg.activationScripts.keyboard.text}
|
${cfg.activationScripts.keyboard.text}
|
||||||
${cfg.activationScripts.fonts.text}
|
${cfg.activationScripts.fonts.text}
|
||||||
${cfg.activationScripts.nvram.text}
|
${cfg.activationScripts.nvram.text}
|
||||||
|
${cfg.activationScripts.files.text}
|
||||||
|
|
||||||
${cfg.activationScripts.postActivation.text}
|
${cfg.activationScripts.postActivation.text}
|
||||||
|
|
||||||
|
|
|
@ -114,6 +114,7 @@ in
|
||||||
|
|
||||||
ln -s ${cfg.build.patches}/patches $out/patches
|
ln -s ${cfg.build.patches}/patches $out/patches
|
||||||
ln -s ${cfg.build.etc}/etc $out/etc
|
ln -s ${cfg.build.etc}/etc $out/etc
|
||||||
|
ln -s ${cfg.build.files} $out/links.json
|
||||||
ln -s ${cfg.path} $out/sw
|
ln -s ${cfg.path} $out/sw
|
||||||
|
|
||||||
mkdir -p $out/Library
|
mkdir -p $out/Library
|
||||||
|
|
63
modules/system/files/default.nix
Normal file
63
modules/system/files/default.nix
Normal 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
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
150
modules/system/files/linker.py
Normal file
150
modules/system/files/linker.py
Normal 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}")
|
52
modules/system/files/write-text.nix
Normal file
52
modules/system/files/write-text.nix
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue