diff --git a/lib/types.nix b/lib/types.nix index 46ed05d288f2e..e155a63276a13 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -224,6 +224,13 @@ rec { merge = mergeOneOption; }; + relativePath = mkOptionType { + name = "relativePath"; + # Hacky: there is no ‘isRelativePath’ primop. + check = x: builtins.substring 0 1 (toString x) != "/"; + merge = mergeOneOption; + }; + # drop this in the future: list = builtins.trace "`types.list` is deprecated; use `types.listOf` instead" types.listOf; diff --git a/nixos/modules/config/nixup.nix b/nixos/modules/config/nixup.nix new file mode 100644 index 0000000000000..7c48db8b79b66 --- /dev/null +++ b/nixos/modules/config/nixup.nix @@ -0,0 +1,71 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + resource-manager = pkgs.stdenv.mkDerivation { + name = "resource-manager"; + buildInputs = with pkgs; [ openssl attr libbsd ]; + buildCommand = '' + mkdir -p $out/bin + gcc -O2 -Wall -lssl -lbsd -o $out/bin/resource-manager ${./resource-manager.c} + ''; + }; + +in + +{ + + options.nixup.enable = mkEnableOption "NixUP"; + + config = mkIf config.nixup.enable { + security.wrappers = { + resource-manager.source = "${resource-manager}/bin/resource-manager"; + }; + + environment.systemPackages = [ config.system.build.nixup-rebuild resource-manager ]; + + environment.sessionVariables = { + NIXUP_USER_PROFILE_DIR = "/nix/var/nix/profiles/nixup/\${USER}"; + }; + + security.pam.services = + let + sessionCommand = '' + PATH=${pkgs.coreutils}/bin:$PATH + + # Set up the per-user profile. + NIXUP_USER_PROFILE_DIR=/nix/var/nix/profiles/nixup/$PAM_USER + if ! test -e $NIXUP_USER_PROFILE_DIR; then + mkdir -m 0755 -p $NIXUP_USER_PROFILE_DIR + chown $(id -u $PAM_USER):$(id -g $PAM_USER) $NIXUP_USER_PROFILE_DIR + fi + + if test "$(stat --printf '%u' $NIXUP_USER_PROFILE_DIR)" != "$(id -u $PAM_USER)"; then + echo "WARNING: bad ownership on $NIXUP_USER_PROFILE_DIR" >&2 + fi + + # Set up the per-user gcroot. + NIXUP_USER_GCROOTS_DIR=/nix/var/nix/gcroots/nixup/$PAM_USER + if ! test -e $NIXUP_USER_GCROOTS_DIR; then + mkdir -m 0755 -p $NIXUP_USER_GCROOTS_DIR + chown $(id -u $PAM_USER):$(id -g $PAM_USER) $NIXUP_USER_GCROOTS_DIR + fi + + if test "$(stat --printf '%u' $NIXUP_USER_GCROOTS_DIR)" != "$(id -u $PAM_USER)"; then + echo "WARNING: bad ownership on $NIXUP_USER_GCROOTS_DIR" >&2 + fi + + # Activate nixup user profile. + if test -e $NIXUP_USER_PROFILE_DIR/default/activate; then + ${pkgs.sudo}/bin/sudo -u $PAM_USER -H NIXUP_RUNTIME_DIR="/run/user/$(id -u $PAM_USER)/nixup" $NIXUP_USER_PROFILE_DIR/default/activate + fi + ''; + in + { + systemd-user = { sessionCommands = sessionCommand; }; + }; + + }; + +} diff --git a/nixos/modules/config/resource-manager.c b/nixos/modules/config/resource-manager.c new file mode 100644 index 0000000000000..19fd29315ad47 --- /dev/null +++ b/nixos/modules/config/resource-manager.c @@ -0,0 +1,821 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +/* + * Linux kernel patches to add fsetxattrat, fgetxattrat, flistxattrat, fremovexattrat syscalls: + * + * https://lkml.org/lkml/2014/1/21/266 + * https://lkml.org/lkml/2014/1/21/264 + * https://lkml.org/lkml/2014/1/21/192 + * https://lkml.org/lkml/2014/1/21/207 + */ + +#ifdef DEBUG +#define DEBUG_TEST 1 +#else +#define DEBUG_TEST 0 +#endif + + +/* + * error handling helper functions + */ + +#define RAISE(code) raise_((code), __FILE__, __LINE__) +noreturn static inline void raise_(int code, const char *file, int line) { + fprintf (stderr, "%s:%d: unexpected error=%d (%s).\n", file, line, code, strerror(code)); + abort(); +} + +#define ENFORCE(ret) enforce((ret), __FILE__, __LINE__) +static inline int enforce(int ret, const char *file, int line) { + if (ret < 0) { + raise_(errno, file, line); + } else { + return ret; + } +} + +#define debug_print(fmt, ...) do { if (DEBUG_TEST) fprintf(stderr, "%s:%d:%s(): " fmt, __FILE__, __LINE__, __func__, __VA_ARGS__); } while (0) + + +// +// global variables +// +#define OPEN_MAX 20 + +#define H_STATIC 0 +#define H_VOLATILE 1 + +static uid_t ruid, euid; +static gid_t rgid, egid; + +static char target_old[PATH_MAX]; +static char target_new[PATH_MAX]; +static char home[PATH_MAX]; + + + + +// +// symbolic link +// + +static void symlink_md5_measure(int fd, char* hash) { + char buf[PATH_MAX]; + ssize_t nbytes; + + buf[PATH_MAX-1] = '\0'; + nbytes = ENFORCE(readlinkat(fd, "", buf, PATH_MAX)); + + if (nbytes == PATH_MAX && buf[PATH_MAX-1] != '\0') { + RAISE(ENAMETOOLONG); + } + + // Calculate MD5 hash of symlink target + + MD5_CTX mdContext; + + MD5_Init (&mdContext); + MD5_Update (&mdContext, buf, nbytes); + MD5_Final ((unsigned char*)hash,&mdContext); +} + + +static void symlink_md5_update(const int fd, const char* path, const char* fn, int setvolatile) { + char hash[2*MD5_DIGEST_LENGTH]; + + if (setvolatile == H_STATIC) { + symlink_md5_measure(fd, hash); + MD5((unsigned char*)fn,strlen(fn),(unsigned char*)&hash[MD5_DIGEST_LENGTH]); + + + ENFORCE(seteuid(euid)); + ENFORCE(setegid(egid)); + // TODO: hope for fsetxattrat system call, and then use fd directly + ENFORCE(lsetxattr(path, "trusted.managed", hash, sizeof(char)*2*MD5_DIGEST_LENGTH, 0)); + ENFORCE(setegid(rgid)); + ENFORCE(seteuid(ruid)); + } else { + MD5((unsigned char*)fn,strlen(fn),(unsigned char*)&hash[0]); + + ENFORCE(seteuid(euid)); + ENFORCE(setegid(egid)); + // TODO: hope for fsetxattrat system call, and then use fd directly + ENFORCE(lsetxattr(path, "trusted.managed", hash, sizeof(char)*MD5_DIGEST_LENGTH, 0)); + ENFORCE(setegid(rgid)); + ENFORCE(seteuid(ruid)); + } +} + + +static int symlink_md5_verify(const int fd, const char* path, const char* fn, int allowvolatile) { + char hash_attr[2*MD5_DIGEST_LENGTH]; + char hash_file[2*MD5_DIGEST_LENGTH]; + int size; + + + ENFORCE(seteuid(euid)); + ENFORCE(setegid(egid)); + // TODO: hope for fsetxattrat system call, and then use fd directly + size = ENFORCE(lgetxattr(path, "trusted.managed", hash_attr, sizeof(char)*2*MD5_DIGEST_LENGTH)); + ENFORCE(setegid(rgid)); + ENFORCE(seteuid(ruid)); + + debug_print("symlink verify: attr hash: `%i`:`%s`\n", size, hash_attr); + + if (size == sizeof(char)*2*MD5_DIGEST_LENGTH) { + symlink_md5_measure(fd, hash_file); + MD5((unsigned char*)fn,strlen(fn),(unsigned char*)&hash_file[MD5_DIGEST_LENGTH]); + + if (strncmp(hash_file, hash_attr, 2*MD5_DIGEST_LENGTH) == 0) { + return 1; + } else { + return 0; + } + } else if (allowvolatile == H_VOLATILE && size == sizeof(char)*MD5_DIGEST_LENGTH) { + MD5((unsigned char*)fn,strlen(fn),(unsigned char*)&hash_file[0]); + if (strncmp(hash_file, hash_attr, MD5_DIGEST_LENGTH) == 0) { + return 1; + } else { + return 0; + } + } else { + return 0; + }; +} + + + +// +// regular file +// + +static void file_md5_measure(int fd, char* hash) { + int fd_; + + fd_ = ENFORCE(dup(fd)); + FILE *inFile = fdopen (fd_, "rb"); + + if (inFile == NULL) { + RAISE(errno); + } + + MD5_CTX mdContext; + unsigned char data[1024]; + int bytes; + + MD5_Init (&mdContext); + while ((bytes = fread (data, 1, 1024, inFile)) != 0) + MD5_Update (&mdContext, data, bytes); + if(ferror(inFile)) + RAISE(ferror(inFile)); + MD5_Final ((unsigned char*)hash,&mdContext); + + ENFORCE(fclose(inFile)); // also closes fd_ +} + + +static void file_md5_update(int fd, const char* fn, int setvolatile) { + char hash[2*MD5_DIGEST_LENGTH]; + + + if (setvolatile == H_STATIC) { + file_md5_measure(fd, hash); + MD5((unsigned char*)fn,strlen(fn),(unsigned char*)&hash[MD5_DIGEST_LENGTH]); + + ENFORCE(seteuid(euid)); + ENFORCE(setegid(egid)); + // TODO: hope for fsetxattrat system call, and then use fd directly + ENFORCE(fsetxattr(fd, "trusted.managed", hash, sizeof(char)*2*MD5_DIGEST_LENGTH, 0)); + ENFORCE(setegid(rgid)); + ENFORCE(seteuid(ruid)); + } else { + MD5((unsigned char*)fn,strlen(fn),(unsigned char*)&hash[0]); + + ENFORCE(seteuid(euid)); + ENFORCE(setegid(egid)); + // TODO: hope for fsetxattrat system call, and then use fd directly + ENFORCE(fsetxattr(fd, "trusted.managed", hash, sizeof(char)*MD5_DIGEST_LENGTH, 0)); + ENFORCE(setegid(rgid)); + ENFORCE(seteuid(ruid)); + } +} + + +static int file_md5_verify(const int fd, const char* fn, int allowvolatile) { + char hash_attr[2*MD5_DIGEST_LENGTH]; + char hash_file[2*MD5_DIGEST_LENGTH]; + int size; + + ENFORCE(seteuid(euid)); + ENFORCE(setegid(egid)); + size = ENFORCE(fgetxattr(fd, "trusted.managed", hash_attr, sizeof(char)*2*MD5_DIGEST_LENGTH)); + ENFORCE(setegid(rgid)); + ENFORCE(seteuid(ruid)); + + if (size == sizeof(char)*2*MD5_DIGEST_LENGTH) { + file_md5_measure(fd, hash_file); + MD5((unsigned char*)fn,strlen(fn),(unsigned char*)&hash_file[MD5_DIGEST_LENGTH]); + + if (strncmp(hash_file, hash_attr, 2*MD5_DIGEST_LENGTH) == 0) { + return 1; + } else { + return 0; + } + } else if (allowvolatile == H_VOLATILE && size == sizeof(char)*MD5_DIGEST_LENGTH) { + MD5((unsigned char*)fn,strlen(fn),(unsigned char*)&hash_file[0]); + if (strncmp(hash_file, hash_attr, MD5_DIGEST_LENGTH) == 0) { + return 1; + } else { + return 0; + } + } else { + return 0; + }; +} + + +static inline int fd_md5_exists(const int fd, const char* path) { + int err; + if (path == NULL) { + ENFORCE(seteuid(euid)); + ENFORCE(setegid(egid)); + err = fgetxattr(fd, "trusted.managed", NULL, 0); + ENFORCE(setegid(rgid)); + ENFORCE(seteuid(ruid)); + } else { + ENFORCE(seteuid(euid)); + ENFORCE(setegid(egid)); + // TODO: hope for fsetxattrat system call, and then use fd directly + err = lgetxattr(path, "trusted.managed", NULL, 0); + ENFORCE(setegid(rgid)); + ENFORCE(seteuid(ruid)); + } + + if (err != -1) + return 1; + else if (errno == ENOATTR) + return 0; + else + RAISE(errno); +} + + +static inline int substr (const char* input, int offset, char* dest) +{ + int input_len = strlen (input); + + if (offset > input_len) + return -1; + + strncpy (dest, input + offset, input_len); + return 0; +} + + +static void create_filename(char* buffer, const char* directory, const char* subdirectory, const char* fn) { + // check for buffer overflow + if (strlen(directory) + strlen(subdirectory) + strlen(fn) + 1 > PATH_MAX) + RAISE(ENAMETOOLONG); + strcat(strcat(strcat(buffer, directory), subdirectory), fn); +} + + +static int dir_empty(int fd) +{ + struct dirent *ent; + int ret = 0; + + int fd_ = dup(fd); + DIR *d = fdopendir(fd_); + if (!d) + RAISE(errno); + + errno = 0; + while ((ent = readdir(d))) { + if (ent == NULL && errno != 0) + RAISE(errno); + if (!strcmp(ent->d_name, ".") || !(strcmp(ent->d_name, ".."))) + continue; + ret = 1; + break; + } + + ENFORCE(closedir(d)); + return ret; +} + + +inline static int rmFiles(const char *pathname, const struct stat *sbuf, int type, struct FTW *ftwb) { + return remove(pathname); +} + +inline static int rmtree(const char *path) { + // Delete the directory and its contents by traversing the tree in reverse order, without crossing mount boundaries and symbolic links + return nftw(path, rmFiles, OPEN_MAX, FTW_DEPTH|FTW_MOUNT|FTW_PHYS); +} + + +static unsigned int read_int(const char *fn, int octal) { + char line[256]; + + FILE* f = fopen(fn, "r"); + if (f == NULL) + RAISE(errno); + if(fgets(line, sizeof(line), f) == NULL) { + printf("ERROR: file ‘%s’ does not contain required data...\n", fn); + abort(); + } + line[strcspn(line, "\r\n")] = '\0'; + ENFORCE(fclose(f)); + unsigned int x; + if (octal) { + if (sscanf(line, "%o", &x) == EOF) + RAISE(errno); + } else { + if (sscanf(line, "%u", &x) == EOF) + RAISE(errno); + } + return x; +} + + + + +static int managed_unlink(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) { + char fn[PATH_MAX] = ""; + int x; + debug_print("fpath: `%s`...\n", fpath); + + // make sure the subsequent call to substr can not have a buffer overflow + if (strlen(fpath) + 1 > PATH_MAX) + RAISE(ENAMETOOLONG); + x = substr(fpath, strlen(target_old) + strlen("/sources"), fn); + + debug_print("filename: `%s`...\n", fn); + + if (x == 0 && strlen(fn) > 0) { + char target_new_fn[PATH_MAX] = ""; + create_filename(target_new_fn, target_new, "/sources", fn); + + struct stat sb_target_new_fn; + // check if new_target also contains a file for the current path + x = lstat(target_new_fn, &sb_target_new_fn); + if (x != 0 && errno == ENOENT) { + char home_fn[PATH_MAX] = ""; + create_filename(home_fn, home, "", fn); + debug_print("home filename: `%s`...\n", home_fn); + + struct stat sb_home_fn; + if (lstat(home_fn, &sb_home_fn)==0) { + int fd_home_fn; + switch (sb_home_fn.st_mode & S_IFMT) { + // symbolic link + case S_IFLNK: + fd_home_fn = ENFORCE(open(home_fn, O_RDONLY | O_PATH | O_NOFOLLOW)); + if (fd_md5_exists(fd_home_fn, home_fn)) { + if (symlink_md5_verify(fd_home_fn, home_fn, fn, H_VOLATILE)) { + printf("removing obsolete symlink ‘%s’...\n", home_fn); + ENFORCE(unlinkat(fd_home_fn, "", 0)); + } else { + printf("WARNING: obsolete symlink ‘%s’ has unmanaged target...\n", home_fn); + } + } else { + printf("WARNING: file ‘%s’ should be managed and it would now be obsolete...\n", home_fn); + } + ENFORCE(close(fd_home_fn)); + break; + // regular file + case S_IFREG: + fd_home_fn = ENFORCE(open(home_fn, O_RDONLY)); + if (fd_md5_exists(fd_home_fn, NULL)) { + if (file_md5_verify(fd_home_fn, fn, H_VOLATILE)) { + printf("removing obsolete and unchanged file ‘%s’...\n", home_fn); + ENFORCE(unlinkat(fd_home_fn, "", 0)); + } else { + printf("WARNING: obsolete file ‘%s’ has unmanaged content...\n", home_fn); + } + } else { + printf("WARNING: file ‘%s’ should be managed and it would now be obsolete...\n", home_fn); + } + ENFORCE(close(fd_home_fn)); + break; + // directory + case S_IFDIR: + fd_home_fn = ENFORCE(open(home_fn, O_RDONLY | O_DIRECTORY)); + if (fd_md5_exists(fd_home_fn, NULL)) { + if (dir_empty(fd_home_fn) == 1) { + printf("removing obsolete and empty directory ‘%s’...\n", home_fn); + ENFORCE(unlinkat(fd_home_fn, "", 0)); + } else if (file_md5_verify(fd_home_fn, fn, H_VOLATILE)) { + printf("removing obsolete and empty directory ‘%s’...\n", home_fn); + ENFORCE(rmtree("$home/$fn")); + } else { + printf("WARNING: obsolete directory ‘%s’ contains unmanaged files...\n", home_fn); + } + } else { + printf("WARNING: file ‘%s’ should be managed and it would now be obsolete...\n", home_fn); + } + ENFORCE(close(fd_home_fn)); + break; + default: + printf("WARNING: file ‘%s’ is of unmanageable type...\n", home_fn); + } + } else { + if (errno == ENOENT) + printf("WARNING: obsolete file ‘%s’ does not exist anymore...\n", home_fn); + else + RAISE(errno); + } + } else if (x != 0) { + RAISE(errno); + } + return 0; + } else { + return 0; + } +} + + + + +static inline int exists(const char* path, struct stat* sb) { + debug_print("check if path `%s` exists...", path); + int x = lstat(path, sb); + if (x == 0) { + return 1; + } else if (x != 0 && errno == ENOENT) { + return 0; + } else { + RAISE(errno); + } +} + + +static void atomic_switch(const char* fn, const char* home_fn_tmp, const char* home_fn) { + int allow_volatile; + + char target_new_volatile[PATH_MAX] = ""; + create_filename(target_new_volatile, target_new, "/volatiles", fn); + debug_print("target new volatile: `%s`...\n", target_new_volatile); + + struct stat sb_target_new_volatile; + // check if new_target also contains a file for the current path + if (exists(target_new_volatile, &sb_target_new_volatile)) { + debug_print("target new `%s` is VOLATILE...\n", home_fn); + allow_volatile = H_VOLATILE; + } else { + debug_print("target new `%s` is STATIC...\n", home_fn); + allow_volatile = H_STATIC; + } + + + struct stat sb_home_fn_tmp; + if (ENFORCE(lstat(home_fn_tmp, &sb_home_fn_tmp))==0) { + int fd_home_fn_tmp; + switch (sb_home_fn_tmp.st_mode & S_IFMT) { + // symbolic link + case S_IFLNK: + debug_print("switch symbolic link `%s`...\n", home_fn_tmp); + fd_home_fn_tmp = ENFORCE(open(home_fn_tmp, O_RDONLY | O_PATH | O_NOFOLLOW)); + symlink_md5_update(fd_home_fn_tmp, home_fn_tmp, fn, allow_volatile); + ENFORCE(close(fd_home_fn_tmp)); + break; + // regular file + case S_IFREG: + debug_print("switch regular file `%s`...\n", home_fn_tmp); + fd_home_fn_tmp = ENFORCE(open(home_fn_tmp, O_RDONLY)); + file_md5_verify(fd_home_fn_tmp, fn, allow_volatile); + ENFORCE(close(fd_home_fn_tmp)); + break; + // directory + case S_IFDIR: + debug_print("switch directory `%s`...\n", home_fn_tmp); + fd_home_fn_tmp = ENFORCE(open(home_fn_tmp, O_RDONLY | O_DIRECTORY)); + file_md5_update(fd_home_fn_tmp, fn, allow_volatile); + ENFORCE(close(fd_home_fn_tmp)); + break; + default: + printf("ERROR: file ‘%s’ is of unmanageable type...\n", home_fn_tmp); + exit(-1); + } + } else { + printf("ERROR: INTERNAL ERROR: ‘%s’ does not exist...\n", home_fn_tmp); + abort(); + } + + + char buffer[PATH_MAX] = ""; + create_filename(buffer, target_new, "/modes", fn); + + struct stat sb_target_new_mode; + if (exists(buffer, &sb_target_new_mode)) { + create_filename(buffer, target_new, "/uids", fn); + unsigned int uid = read_int(buffer, 0); + + create_filename(buffer, target_new, "/gids", fn); + unsigned int gid = read_int(buffer, 0); + + create_filename(buffer, target_new, "/modes", fn); + unsigned int mode = read_int(buffer, 1); + + + ENFORCE(lstat(home_fn_tmp, &sb_home_fn_tmp)); + ENFORCE(lchown(home_fn_tmp, uid, gid)); + + if ((sb_home_fn_tmp.st_mode & S_IFMT) != S_IFLNK); + ENFORCE(chmod(home_fn_tmp, mode)); + } + + ENFORCE(rename(home_fn_tmp, home_fn)); //TODO:: switch to renameat +} + + + +static int managed_link(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) { + debug_print("fpath: `%s`...\n", fpath); + + char fn[PATH_MAX] = ""; + int x; + + // make sure the subsequent call to substr can not have a buffer overflow + if (strlen(fpath) + 1 > PATH_MAX) + RAISE(ENAMETOOLONG); + x = substr(fpath, strlen(target_new) + strlen("/sources"), fn); + + debug_print("filename: `%s`...\n", fn); + + if (x == 0 && strlen(fn) > 0) { + char home_fn[PATH_MAX] = ""; + create_filename(home_fn, home, "", fn); + debug_print("home filename: `%s`...\n", home_fn); + + char home_fn_tmp[PATH_MAX] = ""; + create_filename(home_fn_tmp, home, fn, ".nixup-temporary-managed-resource"); + debug_print("home filename tmp: `%s`...\n", home_fn_tmp); + + struct stat sb_home_fn; + if (typeflag == FTW_D && typeflag == FTW_DP) { + debug_print("linking: target `%s` is a directory...\n", fpath); + if (!exists(home_fn, &sb_home_fn)) { + unlink(home_fn_tmp); + + ENFORCE(mkdir(home_fn_tmp, S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH)); //TODO: hope for O_TMPDIR option in mkdir/open + + int fd_home_fn = ENFORCE(open(home_fn, O_RDONLY | O_DIRECTORY)); + file_md5_update(fd_home_fn, fn, H_VOLATILE); + ENFORCE(close(fd_home_fn)); + + ENFORCE(rename(home_fn_tmp, home_fn)); //TODO:: switch to renameat + } else { + return 0; + } + } else if (typeflag == FTW_F || typeflag == FTW_SL) { + debug_print("linking: target `%s` is a file or a symbolic link...\n", fpath); + // make sure no un-managed data is lost + // check that a managed content did not change and so can safely be overridden + if (exists(home_fn, &sb_home_fn)) { + debug_print("linking: file `%s` exists in home with mode '%lo'...\n", home_fn, (unsigned long) sb_home_fn.st_mode); + int fd_home_fn; + switch (sb_home_fn.st_mode & S_IFMT) { + // symbolic link + case S_IFLNK: + debug_print("linking: existing file `%s` is a symbolic link...\n", home_fn); + fd_home_fn = ENFORCE(open(home_fn, O_RDONLY | O_PATH | O_NOFOLLOW)); + if (fd_md5_exists(fd_home_fn, home_fn)) { + if (1!=symlink_md5_verify(fd_home_fn, home_fn, fn, H_VOLATILE)) { + printf("ERROR: managed symbolic link ‘%s’ contains an unmanaged reference...\n", home_fn); + exit(-1); + } + } else { + printf("ERROR: file ‘%s’ already exists and so path cannot be managed...\n", home_fn); + exit(-1); + } + ENFORCE(close(fd_home_fn)); + break; + // regular file + case S_IFREG: + debug_print("linking: existing file `%s` is a regular file...\n", home_fn); + fd_home_fn = ENFORCE(open(home_fn, O_RDONLY)); + if (fd_md5_exists(fd_home_fn, NULL)) { + if (1!=file_md5_verify(fd_home_fn, fn, H_VOLATILE)) { + printf("ERROR: managed file ‘%s’ contains unmanaged data...\n", home_fn); + exit(-1); + } + } else { + printf("ERROR: file ‘%s’ already exists and so path cannot be managed...\n", home_fn); + exit(-1); + } + ENFORCE(close(fd_home_fn)); + break; + // directory + case S_IFDIR: + debug_print("linking: existing file `%s` is a directory...\n", home_fn); + fd_home_fn = ENFORCE(open(home_fn, O_RDONLY | O_DIRECTORY)); + if (fd_md5_exists(fd_home_fn, NULL)) { + if (file_md5_verify(fd_home_fn, fn, H_VOLATILE)) { + printf("ERROR: managed directory ‘%s’ contains unmanaged data...\n", home_fn); + exit(-1); + } + } else { + printf("ERROR: directory ‘%s’ already exists and so path cannot be managed...\n", home_fn); + exit(-1); + } + ENFORCE(close(fd_home_fn)); + break; + default: + debug_print("linking: existing file `%s` is of type `%lo`...\n", home_fn, (unsigned long) (sb_home_fn.st_mode & S_IFMT)); + printf("ERROR: file ‘%s’ already exists and so path cannot be managed...\n", home_fn); + exit(-1); + } + } + + char target_new_dynamic[PATH_MAX] = ""; + create_filename(target_new_dynamic, target_new, "/dynamics", fn); + debug_print("target new dynamic: `%s`...\n", target_new_dynamic); + + struct stat sb_target_new_dynamic; + if (exists(target_new_dynamic, &sb_target_new_dynamic)) { + debug_print("new target `%s` is dynamic...\n", target_new_dynamic); + int pid = ENFORCE(fork()); + int status; + if (pid == 0) { + // TODO: check whether ftw uses O_CLOXEC and if not switch ftw to fts + // so that all file descriptors can be closed properly before execve + ENFORCE(prctl(PR_SET_PDEATHSIG, SIGTERM)); + ENFORCE(setgroups(1, &rgid)); + ENFORCE(setregid(rgid, rgid)); + ENFORCE(setreuid(rgid, ruid)); + + char *cargv[] = { NULL, home_fn, home_fn_tmp, NULL }; + char *cenvp[] = { NULL }; + ENFORCE(execve(fpath, cargv, cenvp)); + } else { + ENFORCE(waitpid(pid, &status, 0)); + if (WIFEXITED(status) != 0 || WEXITSTATUS(status) != 0) { + printf("ERROR: dynamically executed subroutine `%s` did not succeed...\n", fpath); + abort(); + } + } + + atomic_switch(fn, home_fn_tmp, home_fn); + } else { + char target_new_fn[PATH_MAX] = ""; + create_filename(target_new_fn, target_new, "/sources", fn); + + debug_print("new target `%s` is not dynamic...\n", target_new_fn); + + if ((sb_home_fn.st_mode & S_IFMT) == S_IFDIR) + ENFORCE(rmtree(home_fn)); + + + char target_new_mode[PATH_MAX] = ""; + create_filename(target_new_mode, target_new, "/modes", fn); + + struct stat sb_target_new_mode; + if (exists(target_new_mode, &sb_target_new_mode)) { + unlink(home_fn_tmp); // TODO: proper error handling + + FILE *from = fopen(target_new_fn, "r"); + FILE *to = fopen(home_fn_tmp, "w"); + char buffer[BUFSIZ]; + size_t n; + + debug_print("copy new target `%s` to `%s`...\n", target_new_fn, home_fn_tmp); + while ((n = fread(buffer, sizeof(char), sizeof(buffer), from)) > 0) + { + if (ENFORCE(fwrite(buffer, sizeof(char), n, to)) != n) { + printf("ERROR: failed to copy from `%s` to `%s`...", target_new_fn, home_fn_tmp); + abort(); + } + } + if (ferror(from)) + RAISE(ferror(from)); + + atomic_switch(fn, home_fn_tmp, home_fn); + } else { + char link[PATH_MAX]; + if (realpath(target_new_fn, link) == NULL) + RAISE(errno); + + + debug_print("create link: `%s`...\n", link); + + if ((sb_home_fn.st_mode & S_IFMT) == S_IFLNK) { + debug_print("symbolic link `%s` already exists in home...\n", home_fn); + char buf[PATH_MAX]; + ssize_t nbytes; + + buf[PATH_MAX-1] = '\0'; + nbytes = ENFORCE(readlink(home_fn, buf, PATH_MAX)); + if (nbytes == PATH_MAX && buf[PATH_MAX-1] != '\0') { + RAISE(ENAMETOOLONG); + } + buf[nbytes] = '\0'; + + if (strcmp(buf, link) != 0) { + unlink(home_fn_tmp); + ENFORCE(symlink(link,home_fn_tmp)); + atomic_switch(fn, home_fn_tmp, home_fn); + } else + debug_print("old symbolic link `%s` is identical to new one...\n", home_fn); + + } else { + debug_print("symbolic link `%s` is new in home...\n", home_fn); + unlink(home_fn_tmp); + ENFORCE(symlink(link,home_fn_tmp)); + atomic_switch(fn, home_fn_tmp, home_fn); + } + } + } + } else { + printf("ERROR: INTERNAL ERROR: only directories and files are allowed at `%s`...", fpath); + abort(); + } + } + return 0; +} + + + + + +int main(int argc, char* argv[]) +{ + ruid = getuid(); + rgid = getgid(); + euid = geteuid(); + egid = getegid(); + ENFORCE(setegid(rgid)); + ENFORCE(seteuid(ruid)); + + + if (argc > 1) { + char* command = argv[1]; + if (strcmp(command, "switch")==0 && argc==5) { + strlcpy(home, argv[2], PATH_MAX); + strlcpy(target_old, argv[3], PATH_MAX); + strlcpy(target_new, argv[4], PATH_MAX); + + debug_print("home: `%s`...\n", home); + debug_print("target_old: `%s`...\n", target_old); + debug_print("target_new: `%s`...\n", target_new); + + char target_old_sources[PATH_MAX] = ""; + create_filename(target_old_sources, target_old, "/sources", ""); + nftw(target_old_sources, managed_unlink, OPEN_MAX, FTW_DEPTH|FTW_MOUNT|FTW_PHYS); + + char target_new_sources[PATH_MAX] = ""; + create_filename(target_new_sources, target_new, "/sources", ""); + nftw(target_new_sources, managed_link, OPEN_MAX, FTW_MOUNT|FTW_PHYS); + } else if (strcmp(command, "link")==0 && argc==4) { + strlcpy(home, argv[2], PATH_MAX); + strlcpy(target_old, "", PATH_MAX); + strlcpy(target_new, argv[3], PATH_MAX); + + char target_new_sources[PATH_MAX] = ""; + create_filename(target_new_sources, target_new, "/sources", ""); + nftw(target_new_sources, managed_link, OPEN_MAX, FTW_MOUNT|FTW_PHYS); + } else if (strcmp(command, "unlink")==0 && argc==4) { + strlcpy(home, argv[2], PATH_MAX); + strlcpy(target_old, argv[3], PATH_MAX); + strlcpy(target_new, "", PATH_MAX); + + char target_old_sources[PATH_MAX] = ""; + create_filename(target_old_sources, target_old, "/sources", ""); + nftw(target_old_sources, managed_unlink, OPEN_MAX, FTW_DEPTH|FTW_MOUNT|FTW_PHYS); + } else if (strcmp(command, "cleanup")==0 && argc==3) { + strlcpy(home, argv[2], PATH_MAX); + strlcpy(target_old, "", PATH_MAX); + strlcpy(target_new, "", PATH_MAX); + //TODO + } else { + printf("Incorrect usage! Valid commands are 'switch', 'link', 'unlink' or 'cleanup' with the corresponding parameters\n"); + exit(-1); + } + } else { + printf("Incorrect usage! Valid commands are 'switch', 'link', 'unlink' or 'cleanup' with the corresponding parameters\n"); + exit(-1); + } + + exit(0); +} diff --git a/nixos/modules/config/system-environment.nix b/nixos/modules/config/system-environment.nix index 6011e354ece48..15d8dee74c241 100644 --- a/nixos/modules/config/system-environment.nix +++ b/nixos/modules/config/system-environment.nix @@ -34,7 +34,7 @@ in system.build.pamEnvironment = pkgs.writeText "pam-environment" '' ${concatStringsSep "\n" ( - (mapAttrsToList (n: v: ''${n}="${concatStringsSep ":" v}"'') + (mapAttrsToList (n: v: ''${n} DEFAULT="${concatStringsSep ":" v}"'') (zipAttrsWith (const concatLists) ([ (mapAttrs (n: v: [ v ]) cfg.sessionVariables) ]))))} ''; diff --git a/nixos/modules/installer/tools/nixos-rebuild.sh b/nixos/modules/installer/tools/nix__-rebuild.sh similarity index 65% rename from nixos/modules/installer/tools/nixos-rebuild.sh rename to nixos/modules/installer/tools/nix__-rebuild.sh index 9ede74a54cd72..9c25c261810f0 100644 --- a/nixos/modules/installer/tools/nixos-rebuild.sh +++ b/nixos/modules/installer/tools/nix__-rebuild.sh @@ -4,12 +4,24 @@ if [ -x "@shell@" ]; then export SHELL="@shell@"; fi; set -e +flavour=@flavour@ + showSyntax() { - exec man nixos-rebuild + if [ "$flavour" = nixos ]; then + exec man nixos-rebuild + else + echo "Nixuser does not have a help, yet. Please help to improve it!" + fi exit 1 } +# Add a default value for to NIX_PATH. +if [[ "$flavour" = nixup && $NIX_PATH != *"nixup-config"* ]]; then + export NIX_PATH=$NIX_PATH:nixup-config=${XDG_CONFIG_HOME:-$HOME/.config}/nixup/profile.nix +fi + + # Parse the command line. origArgs=("$@") extraBuildFlags=() @@ -19,73 +31,80 @@ fast= rollback= upgrade= repair= -profile=/nix/var/nix/profiles/system buildHost= targetHost= +if [ "$flavour" = nixos ]; then + profile=/nix/var/nix/profiles/system +else + profile=/nix/var/nix/profiles/nixup/$(whoami)/default +fi while [ "$#" -gt 0 ]; do i="$1"; shift 1 - case "$i" in - --help) + case "$i"_@flavour@ in + --help_*) showSyntax ;; - switch|boot|test|build|dry-build|dry-run|dry-activate|build-vm|build-vm-with-bootloader) + switch_*|boot_nixos|login_nixup|test_nixos|build_*|dry-build_nixos|dry-run_*|dry-activate_nixos|build-vm_nixos|build-vm-with-bootloader_nixos|edit_nixos) if [ "$i" = dry-run ]; then i=dry-build; fi action="$i" ;; - --install-grub) + --install-grub_nixos) echo "$0: --install-grub deprecated, use --install-bootloader instead" >&2 export NIXOS_INSTALL_BOOTLOADER=1 ;; - --install-bootloader) + --install-bootloader_nixos) export NIXOS_INSTALL_BOOTLOADER=1 ;; - --no-build-nix) + --no-build-nix_nixos) buildNix= ;; - --rollback) + --rollback_*) rollback=1 ;; - --upgrade) + --upgrade_nixos) upgrade=1 ;; - --repair) + --repair_nixos) repair=1 extraBuildFlags+=("$i") ;; - --max-jobs|-j|--cores|-I) + --max-jobs_*|-j_*|--cores_*|-I_*) j="$1"; shift 1 extraBuildFlags+=("$i" "$j") ;; - --show-trace|--no-build-hook|--keep-failed|-K|--keep-going|-k|--verbose|-v|-vv|-vvv|-vvvv|-vvvvv|--fallback|--repair|--no-build-output|-Q|-j*) + --show-trace_*|--no-build-hook_*|--keep-failed_*|-K_*|--keep-going_*|-k_*|--verbose_*|-v_*|-vv_*|-vvv_*|-vvvv_*|-vvvvv_*|--fallback_*|--repair_*|--no-build-output_*|-Q_*|-j*) extraBuildFlags+=("$i") ;; - --option) + --option_*) j="$1"; shift 1 k="$1"; shift 1 extraBuildFlags+=("$i" "$j" "$k") ;; - --fast) + --fast_nixos) buildNix= fast=1 extraBuildFlags+=(--show-trace) ;; - --profile-name|-p) + --profile-name_*|-p_*) if [ -z "$1" ]; then echo "$0: ‘--profile-name’ requires an argument" exit 1 fi - if [ "$1" != system ]; then + if [ "$flavour" = nixos -a "$1" != system ]; then profile="/nix/var/nix/profiles/system-profiles/$1" mkdir -p -m 0755 "$(dirname "$profile")" fi + if [ "$flavour" = nixup -a "$1" != default ]; then + profile="/nix/var/nix/profiles/nixup/$(whoami)/$1" + fi shift 1 ;; - --build-host|h) + --build-host_nixos|h_nixos) buildHost="$1" shift 1 ;; - --target-host|t) + --target-host_nixos|t_nixos) targetHost="$1" shift 1 ;; @@ -137,6 +156,15 @@ copyToTarget() { fi } +if [ "$flavour" = nixup -a "$action" = edit ]; then + if [ -n "$EDITOR" ]; then + "$EDITOR" "${XDG_CONFIG_HOME:-$HOME/.config}/nixup/profile.nix" + else + echo "ERROR: \$EDITOR environment variable not set!" + fi + exit 0 +fi + nixBuild() { if [ -z "$buildHost" ]; then nix-build "$@" @@ -184,18 +212,18 @@ nixBuild() { if [ -z "$action" ]; then showSyntax; fi # Only run shell scripts from the Nixpkgs tree if the action is -# "switch", "boot", or "test". With other actions (such as "build"), -# the user may reasonably expect that no code from the Nixpkgs tree is -# executed, so it's safe to run nixos-rebuild against a potentially -# untrusted tree. +# "switch", "boot", or "test" of nixos or "switch" or "dry-run" for nixup. +# With other actions (such as "build"), the user may reasonably expect that +# no code from the Nixpkgs tree is executed, so it's safe to run +# nixos-rebuild against a potentially untrusted tree. canRun= -if [ "$action" = switch -o "$action" = boot -o "$action" = test ]; then +if [ "$action" = switch -o "$action" = test -o "$action" = boot ]; then canRun=1 fi -# If ‘--upgrade’ is given, run ‘nix-channel --update nixos’. -if [ -n "$upgrade" -a -z "$_NIXOS_REBUILD_REEXEC" ]; then +# If ‘--upgrade’ is given, run ‘nix-channel --update @flavour@’. +if [ "$flavour" = nixos -a -n "$upgrade" -a -z "$_NIXOS_REBUILD_REEXEC" ]; then nix-channel --update nixos # If there are other channels that contain a file called @@ -214,12 +242,12 @@ fi # amd64 system the user has an i686 Nix package in her PATH, then we # would silently downgrade the whole system to be i686 NixOS on the # next reboot. -if [ -z "$_NIXOS_REBUILD_REEXEC" ]; then +if [ "$flavour" = nixos -a -z "$_NIXOS_REBUILD_REEXEC" ]; then export PATH=@nix@/bin:$PATH fi # Re-execute nixos-rebuild from the Nixpkgs tree. -if [ -z "$_NIXOS_REBUILD_REEXEC" -a -n "$canRun" -a -z "$fast" ]; then +if [ "$flavour" = nixos -a -z "$_NIXOS_REBUILD_REEXEC" -a -n "$canRun" -a -z "$fast" ]; then if p=$(nix-build --no-out-link --expr 'with import {}; config.system.build.nixos-rebuild' "${extraBuildFlags[@]}"); then export _NIXOS_REBUILD_REEXEC=1 exec $p/bin/nixos-rebuild "${origArgs[@]}" @@ -228,7 +256,7 @@ if [ -z "$_NIXOS_REBUILD_REEXEC" -a -n "$canRun" -a -z "$fast" ]; then fi -tmpDir=$(mktemp -t -d nixos-rebuild.XXXXXX) +tmpDir=$(mktemp -t -d @flavour@-rebuild.XXXXXX) SSHOPTS="$NIX_SSHOPTS -o ControlMaster=auto -o ControlPath=$tmpDir/ssh-%n -o ControlPersist=60" cleanup() { @@ -277,23 +305,25 @@ remotePATH= if [ -n "$buildNix" ]; then echo "building Nix..." >&2 nixDrv= - if ! nixDrv="$(nix-instantiate '' --add-root $tmpDir/nix.drv --indirect -A config.nix.package.out "${extraBuildFlags[@]}")"; then - if ! nixDrv="$(nix-instantiate '' --add-root $tmpDir/nix.drv --indirect -A nix "${extraBuildFlags[@]}")"; then - nixStorePath="$(prebuiltNix "$(uname -m)")" - if ! nix-store -r $nixStorePath --add-root $tmpDir/nix --indirect \ - --option extra-binary-caches https://cache.nixos.org/; then - echo "warning: don't know how to get latest Nix" >&2 - fi - # Older version of nix-store -r don't support --add-root. - [ -e $tmpDir/nix ] || ln -sf $nixStorePath $tmpDir/nix - if [ -n "$buildHost" ]; then - remoteNixStorePath="$(prebuiltNix "$(buildHostCmd uname -m)")" - remoteNix="$remoteNixStorePath/bin" - if ! buildHostCmd nix-store -r $remoteNixStorePath \ - --option extra-binary-caches https://cache.nixos.org/ >/dev/null; then - remoteNix= + if ! test "$flavour" = nixup && nixDrv="$(nix-instantiate '' --add-root $tmpDir/nix.drv --indirect -A config.nix.package.out "${extraBuildFlags[@]}")"; then + if ! nixDrv="$(nix-instantiate '' --add-root $tmpDir/nix.drv --indirect -A config.nix.package.out "${extraBuildFlags[@]}")"; then + if ! nixDrv="$(nix-instantiate '' --add-root $tmpDir/nix.drv --indirect -A nix "${extraBuildFlags[@]}")"; then + nixStorePath="$(prebuiltNix "$(uname -m)")" + if ! nix-store -r $nixStorePath --add-root $tmpDir/nix --indirect \ + --option extra-binary-caches https://cache.nixos.org/; then echo "warning: don't know how to get latest Nix" >&2 fi + # Older version of nix-store -r don't support --add-root. + [ -e $tmpDir/nix ] || ln -sf $nixStorePath $tmpDir/nix + if [ -n "$buildHost" ]; then + remoteNixStorePath="$(prebuiltNix "$(buildHostCmd uname -m)")" + remoteNix="$remoteNixStorePath/bin" + if ! buildHostCmd nix-store -r $remoteNixStorePath \ + --option extra-binary-caches https://cache.nixos.org/ >/dev/null; then + remoteNix= + echo "warning: don't know how to get latest Nix" >&2 + fi + fi fi fi fi @@ -313,7 +343,7 @@ fi # Update the version suffix if we're building from Git (so that # nixos-version shows something useful). -if [ -n "$canRun" ]; then +if [ "$flavour" = nixos -a -n "$canRun" ]; then if nixpkgs=$(nix-instantiate --find-file nixpkgs "${extraBuildFlags[@]}"); then suffix=$($SHELL $nixpkgs/nixos/modules/installer/tools/get-version-suffix "${extraBuildFlags[@]}" || true) if [ -n "$suffix" ]; then @@ -332,16 +362,25 @@ fi # or "boot"), or just build it and create a symlink "result" in the # current directory (for "build" and "test"). if [ -z "$rollback" ]; then - echo "building the system configuration..." >&2 - if [ "$action" = switch -o "$action" = boot ]; then - pathToConfig="$(nixBuild '' --no-out-link -A system "${extraBuildFlags[@]}")" + echo "building the @flavour@ configuration..." >&2 + if [ "$action" = switch -o "$action" = boot -o "$action" = login ]; then + if [ "$flavour" = nixos ]; then + pathToConfig="$(nixBuild '' --no-out-link -A system "${extraBuildFlags[@]}")" + else + pathToConfig="$(nixBuild '' --no-out-link -A profile "${extraBuildFlags[@]}")" + fi copyToTarget "$pathToConfig" targetHostCmd nix-env -p "$profile" --set "$pathToConfig" elif [ "$action" = test -o "$action" = build -o "$action" = dry-build -o "$action" = dry-activate ]; then - pathToConfig="$(nixBuild '' -A system -k "${extraBuildFlags[@]}")" - elif [ "$action" = build-vm ]; then + if [ "$flavour" = nixos ]; then + pathToConfig="$(nixBuild '' -A system -k "${extraBuildFlags[@]}")" + else + pathToConfig="$(nixBuild '' -A profile -k "${extraBuildFlags[@]}")" + fi + pathToConfig=./result + elif [ "$flavour" = nixos -a "$action" = build-vm ]; then pathToConfig="$(nixBuild '' -A vm -k "${extraBuildFlags[@]}")" - elif [ "$action" = build-vm-with-bootloader ]; then + elif [ "$flavour" = nixos -a "$action" = build-vm-with-bootloader ]; then pathToConfig="$(nixBuild '' -A vmWithBootLoader -k "${extraBuildFlags[@]}")" else showSyntax @@ -351,7 +390,7 @@ if [ -z "$rollback" ]; then copyToTarget "$pathToConfig" fi else # [ -n "$rollback" ] - if [ "$action" = switch -o "$action" = boot ]; then + if [ "$action" = switch -o "$action" = boot -o "$action" = login ]; then targetHostCmd nix-env --rollback -p "$profile" pathToConfig="$profile" elif [ "$action" = test -o "$action" = build ]; then @@ -369,9 +408,9 @@ else # [ -n "$rollback" ] fi -# If we're not just building, then make the new configuration the boot +# If we're not just building, then make the new configuration the boot/login # default and/or activate it now. -if [ "$action" = switch -o "$action" = boot -o "$action" = test -o "$action" = dry-activate ]; then +if [ "$action" = switch -o "$action" = boot -o "$action" = login -o "$action" = test -o "$action" = dry-activate ]; then if ! targetHostCmd $pathToConfig/bin/switch-to-configuration "$action"; then echo "warning: error(s) occurred while switching to the new configuration" >&2 exit 1 @@ -379,7 +418,7 @@ if [ "$action" = switch -o "$action" = boot -o "$action" = test -o "$action" = d fi -if [ "$action" = build-vm ]; then +if [ "$flavour" = nixos -a "$action" = build-vm ]; then cat >&2 < ''${XDG_CONFIG_HOME:-$HOME/.config}/nixup/profile.nix + fi + ''} # Subscribe the root user to the NixOS channel by default. if [ "$USER" = root -a ! -e $HOME/.nix-channels ]; then diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix index 5ded36329f333..1ed5abde02711 100644 --- a/nixos/modules/security/pam.nix +++ b/nixos/modules/security/pam.nix @@ -1,4 +1,5 @@ # This module provides configuration for the PAM (Pluggable + # Authentication Modules) system. { config, lib, pkgs, ... }: @@ -93,6 +94,19 @@ let ''; }; + sessionCommands = mkOption { + type = types.nullOr types.lines; + default = null; + description = '' + Content of a BASH script that gets executed when the user + session is started. The scripts gets executed right + before the systemd user session is started, but after + the environment variables have been set through PAM. + The script is executed as root! The output of the script + is dumped into /dev/null. + ''; + }; + startSession = mkOption { default = false; type = types.bool; @@ -315,7 +329,7 @@ let # Session management. ${optionalString cfg.setEnvironment '' - session required pam_env.so envfile=${config.system.build.pamEnvironment} + session required pam_env.so readenv=0 conffile=${config.system.build.pamEnvironment} ''} session required pam_unix.so ${optionalString cfg.setLoginUid @@ -336,6 +350,10 @@ let "session optional ${pam_krb5}/lib/security/pam_krb5.so"} ${optionalString cfg.otpwAuth "session optional ${pkgs.otpw}/lib/security/pam_otpw.so"} + ${optionalString cfg.oathAuth + "session optional ${pkgs.oathToolkit}/lib/security/pam_oath.so window=5 usersfile=/etc/users.oath"} + ${optionalString (cfg.sessionCommands != null) + "session optional ${pkgs.pam}/lib/security/pam_exec.so type=open_session ${writeSessionCommands cfg.sessionCommands}"} ${optionalString cfg.startSession "session optional ${pkgs.systemd}/lib/security/pam_systemd.so"} ${optionalString cfg.forwardXAuth @@ -371,6 +389,12 @@ let motd = pkgs.writeText "motd" config.users.motd; + writeSessionCommands = content: pkgs.writeScript "pam-session-commands.sh" '' + #!${pkgs.stdenv.shell} -eux + + ${content} + ''; + makePAMService = pamService: { source = pkgs.writeText "${pamService.name}.pam" pamService.text; target = "pam.d/${pamService.name}"; diff --git a/nixos/modules/services/misc/nix-daemon.nix b/nixos/modules/services/misc/nix-daemon.nix index beca820d2d602..15cd44b4b2328 100644 --- a/nixos/modules/services/misc/nix-daemon.nix +++ b/nixos/modules/services/misc/nix-daemon.nix @@ -448,6 +448,8 @@ in /nix/var/log/nix/drvs \ /nix/var/nix/channel-cache mkdir -m 1777 -p \ + /nix/var/nix/gcroots/nixup \ + /nix/var/nix/profiles/nixup \ /nix/var/nix/gcroots/per-user \ /nix/var/nix/profiles/per-user \ /nix/var/nix/gcroots/tmp diff --git a/nixos/modules/services/x11/display-managers/gdm.nix b/nixos/modules/services/x11/display-managers/gdm.nix index e83f26516f5f8..7a83b2b5bc974 100644 --- a/nixos/modules/services/x11/display-managers/gdm.nix +++ b/nixos/modules/services/x11/display-managers/gdm.nix @@ -185,7 +185,7 @@ in password required pam_deny.so session required pam_succeed_if.so audit quiet_success user = gdm - session required pam_env.so envfile=${config.system.build.pamEnvironment} + session required pam_env.so readenv=0 conffile=${config.system.build.pamEnvironment} session optional ${pkgs.systemd}/lib/security/pam_systemd.so session optional pam_keyinit.so force revoke session optional pam_permit.so @@ -193,7 +193,7 @@ in gdm.text = '' auth requisite pam_nologin.so - auth required pam_env.so envfile=${config.system.build.pamEnvironment} + auth required pam_env.so readenv=0 conffile=${config.system.build.pamEnvironment} auth required pam_succeed_if.so uid >= 1000 quiet auth optional ${pkgs.gnome3.gnome_keyring}/lib/security/pam_gnome_keyring.so @@ -210,7 +210,7 @@ in ${optionalString config.security.pam.enableEcryptfs "password optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so"} - session required pam_env.so envfile=${config.system.build.pamEnvironment} + session required pam_env.so readenv=0 conffile=${config.system.build.pamEnvironment} session required pam_unix.so ${optionalString config.security.pam.enableEcryptfs "session optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so"} @@ -221,7 +221,7 @@ in gdm-password.text = '' auth requisite pam_nologin.so - auth required pam_env.so envfile=${config.system.build.pamEnvironment} + auth required pam_env.so readenv=0 conffile=${config.system.build.pamEnvironment} auth required pam_succeed_if.so uid >= 1000 quiet auth optional ${pkgs.gnome3.gnome_keyring}/lib/security/pam_gnome_keyring.so @@ -237,7 +237,7 @@ in ${optionalString config.security.pam.enableEcryptfs "password optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so"} - session required pam_env.so envfile=${config.system.build.pamEnvironment} + session required pam_env.so readenv=0 conffile=${config.system.build.pamEnvironment} session required pam_unix.so ${optionalString config.security.pam.enableEcryptfs "session optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so"} @@ -257,7 +257,7 @@ in password requisite pam_unix.so nullok sha512 session optional pam_keyinit.so revoke - session required pam_env.so envfile=${config.system.build.pamEnvironment} + session required pam_env.so readenv=0 conffile=${config.system.build.pamEnvironment} session required pam_unix.so session required pam_loginuid.so session optional ${pkgs.systemd}/lib/security/pam_systemd.so diff --git a/nixos/modules/services/x11/display-managers/lightdm.nix b/nixos/modules/services/x11/display-managers/lightdm.nix index 1733f2fd39b27..14bcd9591b390 100644 --- a/nixos/modules/services/x11/display-managers/lightdm.nix +++ b/nixos/modules/services/x11/display-managers/lightdm.nix @@ -216,14 +216,14 @@ in allowNullPassword = true; startSession = true; text = '' - auth required pam_env.so envfile=${config.system.build.pamEnvironment} + auth required pam_env.so readenv=0 conffile=${config.system.build.pamEnvironment} auth required pam_permit.so account required pam_permit.so password required pam_deny.so - session required pam_env.so envfile=${config.system.build.pamEnvironment} + session required pam_env.so readenv=0 conffile=${config.system.build.pamEnvironment} session required pam_unix.so session optional ${pkgs.systemd}/lib/security/pam_systemd.so ''; diff --git a/nixos/modules/services/x11/display-managers/sddm.nix b/nixos/modules/services/x11/display-managers/sddm.nix index facaea131ae51..3d9ca4cd20e79 100644 --- a/nixos/modules/services/x11/display-managers/sddm.nix +++ b/nixos/modules/services/x11/display-managers/sddm.nix @@ -234,7 +234,7 @@ in password required pam_deny.so session required pam_succeed_if.so audit quiet_success user = sddm - session required pam_env.so envfile=${config.system.build.pamEnvironment} + session required pam_env.so readenv=0 conffile=${config.system.build.pamEnvironment} session optional ${pkgs.systemd}/lib/security/pam_systemd.so session optional pam_keyinit.so force revoke session optional pam_permit.so diff --git a/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl index 29cc60b00324b..ade3d014e98f9 100644 --- a/nixos/modules/system/activation/switch-to-configuration.pl +++ b/nixos/modules/system/activation/switch-to-configuration.pl @@ -9,20 +9,38 @@ my $out = "@out@"; +my $flavour = "@flavour@"; +my %flavour; + +my $nixup_runtime_dir; + +if ($flavour eq "nixos") { + $flavour{'commandPrefix'} = "/run/systemd/"; + $flavour{'mode'} = "boot"; + $flavour{'commandFlag'} = "--system"; +} elsif ($flavour eq "nixup") { + $nixup_runtime_dir = $ENV{'NIXUP_RUNTIME_DIR'} or die "ERROR: NIXUP_RUNTIME_DIR not set"; + $flavour{'commandPrefix'} = "$nixup_runtime_dir/systemd-"; + $flavour{'mode'} = "login"; + $flavour{'commandFlag'} = "--user"; +} else { + die; +} + # To be robust against interruption, record what units need to be started etc. -my $startListFile = "/run/systemd/start-list"; -my $restartListFile = "/run/systemd/restart-list"; -my $reloadListFile = "/run/systemd/reload-list"; +my $startListFile = "$flavour{'commandPrefix'}start-list"; +my $restartListFile = "$flavour{'commandPrefix'}restart-list"; +my $reloadListFile = "$flavour{'commandPrefix'}reload-list"; my $action = shift @ARGV; -if (!defined $action || ($action ne "switch" && $action ne "boot" && $action ne "test" && $action ne "dry-activate")) { +if (!defined $action || ($action ne "switch" && $action ne "$flavour{'mode'}" && $action ne "test" && $action ne "dry-activate")) { print STDERR < 'quiet') // "") =~ /ID=nixos/s; + $flavour eq "nixup" || -f "/etc/NIXOS" || (read_file("/etc/os-release", err_mode => 'quiet') // "") =~ /ID=nixos/s; -openlog("nixos", "", LOG_USER); +openlog("$flavour", "", LOG_USER); # Install or update the bootloader. -if ($action eq "switch" || $action eq "boot") { +if ($flavour eq "nixos" && ($action eq "switch" || $action eq "boot")) { system("@installBootLoader@ $out") == 0 or exit 1; } # Just in case the new configuration hangs the system, do a sync now. -system("@coreutils@/bin/sync", "-f", "/nix/store") unless ($ENV{"NIXOS_NO_SYNC"} // "") eq "1"; +system("@coreutils@/bin/sync", "-f", "/nix/store") unless ($ENV{uc($flavour)."_NO_SYNC"} // "") eq "1"; -exit 0 if $action eq "boot"; +exit 0 if $action eq $flavour{'mode'}; # Check if we can activate the new configuration. -my $oldVersion = read_file("/run/current-system/init-interface-version", err_mode => 'quiet') // ""; -my $newVersion = read_file("$out/init-interface-version"); +if ($flavour eq "nixos") { + my $oldVersion = read_file("/run/current-system/init-interface-version", err_mode => 'quiet') // ""; + my $newVersion = read_file("$out/init-interface-version"); -if ($newVersion ne $oldVersion) { - print STDERR <{$mountPoint}; - my $new = $newFss->{$mountPoint}; - my $unit = pathToUnitName($mountPoint); - if (!defined $new) { - # Filesystem entry disappeared, so unmount it. - $unitsToStop{$unit} = 1; - } elsif ($prev->{fsType} ne $new->{fsType} || $prev->{device} ne $new->{device}) { - # Filesystem type or device changed, so unmount and mount it. - $unitsToStop{$unit} = 1; - $unitsToStart{$unit} = 1; - recordUnit($startListFile, $unit); - } elsif ($prev->{options} ne $new->{options}) { - # Mount options changes, so remount it. - $unitsToReload{$unit} = 1; - recordUnit($reloadListFile, $unit); +if ($flavour eq "nixos") { + # Compare the previous and new fstab to figure out which filesystems + # need a remount or need to be unmounted. New filesystems are mounted + # automatically by starting local-fs.target. FIXME: might be nicer if + # we generated units for all mounts; then we could unify this with the + # unit checking code above. + my ($prevFss, $prevSwaps) = parseFstab "/etc/fstab"; + my ($newFss, $newSwaps) = parseFstab "$out/etc/fstab"; + foreach my $mountPoint (keys %$prevFss) { + my $prev = $prevFss->{$mountPoint}; + my $new = $newFss->{$mountPoint}; + my $unit = pathToUnitName($mountPoint); + if (!defined $new) { + # Filesystem entry disappeared, so unmount it. + $unitsToStop{$unit} = 1; + } elsif ($prev->{fsType} ne $new->{fsType} || $prev->{device} ne $new->{device}) { + # Filesystem type or device changed, so unmount and mount it. + $unitsToStop{$unit} = 1; + $unitsToStart{$unit} = 1; + recordUnit($startListFile, $unit); + } elsif ($prev->{options} ne $new->{options}) { + # Mount options changes, so remount it. + $unitsToReload{$unit} = 1; + recordUnit($reloadListFile, $unit); + } } -} - -# Also handles swap devices. -foreach my $device (keys %$prevSwaps) { - my $prev = $prevSwaps->{$device}; - my $new = $newSwaps->{$device}; - if (!defined $new) { - # Swap entry disappeared, so turn it off. Can't use - # "systemctl stop" here because systemd has lots of alias - # units that prevent a stop from actually calling - # "swapoff". - print STDERR "stopping swap device: $device\n"; - system("@utillinux@/sbin/swapoff", $device); + + # Also handles swap devices. + foreach my $device (keys %$prevSwaps) { + my $prev = $prevSwaps->{$device}; + my $new = $newSwaps->{$device}; + if (!defined $new) { + # Swap entry disappeared, so turn it off. Can't use + # "systemctl stop" here because systemd has lots of alias + # units that prevent a stop from actually calling + # "swapoff". + print STDERR "stopping swap device: $device\n"; + system("@utillinux@/sbin/swapoff", $device); + } + # FIXME: update swap options (i.e. its priority). } - # FIXME: update swap options (i.e. its priority). } - -# Should we have systemd re-exec itself? -my $prevSystemd = abs_path("/proc/1/exe") // "/unknown"; -my $newSystemd = abs_path("@systemd@/lib/systemd/systemd") or die; -my $restartSystemd = $prevSystemd ne $newSystemd; +my $restartSystemd; +if ($flavour eq "nixos") { + # Should we have systemd re-exec itself? + my $prevSystemd = abs_path("/proc/1/exe") // "/unknown"; + my $newSystemd = abs_path("@systemd@/lib/systemd/systemd") or die; + $restartSystemd = $prevSystemd ne $newSystemd; +} else { + $restartSystemd = 0; +}; sub filterUnits { @@ -359,12 +401,12 @@ sub filterUnits { } -syslog(LOG_NOTICE, "switching to system configuration $out"); +syslog(LOG_NOTICE, "switching to $flavour configuration $out"); if (scalar (keys %unitsToStop) > 0) { print STDERR "stopping the following units: ", join(", ", @unitsToStopFiltered), "\n" if scalar @unitsToStopFiltered; - system("systemctl", "stop", "--", sort(keys %unitsToStop)); # FIXME: ignore errors? + system("@systemd@/bin/systemctl", "$flavour{'commandFlag'}", "stop", "--", sort(keys %unitsToStop)); # FIXME: ignore errors? } print STDERR "NOT restarting the following changed units: ", join(", ", sort(keys %unitsToSkip)), "\n" @@ -379,24 +421,26 @@ sub filterUnits { # Restart systemd if necessary. if ($restartSystemd) { print STDERR "restarting systemd...\n"; - system("@systemd@/bin/systemctl", "daemon-reexec") == 0 or $res = 2; + system("@systemd@/bin/systemctl", "$flavour{'commandFlag'}", "daemon-reexec") == 0 or $res = 2; } # Forget about previously failed services. -system("@systemd@/bin/systemctl", "reset-failed"); +system("@systemd@/bin/systemctl", "$flavour{'commandFlag'}", "reset-failed"); # Make systemd reload its units. -system("@systemd@/bin/systemctl", "daemon-reload") == 0 or $res = 3; +system("@systemd@/bin/systemctl", "$flavour{'commandFlag'}", "daemon-reload") == 0 or $res = 3; -# Set the new tmpfiles -print STDERR "setting up tmpfiles\n"; -system("@systemd@/bin/systemd-tmpfiles", "--create", "--remove", "--exclude-prefix=/dev") == 0 or $res = 3; +if ($flavour eq "nixos") { + # Set the new tmpfiles + print STDERR "setting up tmpfiles\n"; + system("@systemd@/bin/systemd-tmpfiles", "--create", "--remove", "--exclude-prefix=/dev") == 0 or $res = 3; +} # Reload units that need it. This includes remounting changed mount # units. if (scalar(keys %unitsToReload) > 0) { print STDERR "reloading the following units: ", join(", ", sort(keys %unitsToReload)), "\n"; - system("@systemd@/bin/systemctl", "reload", "--", sort(keys %unitsToReload)) == 0 or $res = 4; + system("@systemd@/bin/systemctl", "$flavour{'commandFlag'}", "reload", "--", sort(keys %unitsToReload)) == 0 or $res = 4; unlink($reloadListFile); } @@ -404,7 +448,7 @@ sub filterUnits { # than stopped and started). if (scalar(keys %unitsToRestart) > 0) { print STDERR "restarting the following units: ", join(", ", sort(keys %unitsToRestart)), "\n"; - system("@systemd@/bin/systemctl", "restart", "--", sort(keys %unitsToRestart)) == 0 or $res = 4; + system("@systemd@/bin/systemctl", "$flavour{'commandFlag'}", "restart", "--", sort(keys %unitsToRestart)) == 0 or $res = 4; unlink($restartListFile); } @@ -414,10 +458,38 @@ sub filterUnits { # that are symlinks to other units. We shouldn't start both at the # same time because we'll get a "Failed to add path to set" error from # systemd. -print STDERR "starting the following units: ", join(", ", @unitsToStartFiltered), "\n" - if scalar @unitsToStartFiltered; -system("@systemd@/bin/systemctl", "start", "--", sort(keys %unitsToStart)) == 0 or $res = 4; -unlink($startListFile); +if (scalar(keys %unitsToStart) > 0) { + print STDERR "starting the following units: ", join(", ", @unitsToStartFiltered), "\n" + if scalar @unitsToStartFiltered; + system("@systemd@/bin/systemctl", "$flavour{'commandFlag'}", "start", "--", sort(keys %unitsToStart)) == 0 or $res = 4; + unlink($startListFile); +} + +sub getEnabledUnits { + # FIXME: use D-Bus or whatever to query this, since parsing the + # output of list-units is likely to break. + my $lines = `LANG= @systemd@/bin/systemctl $flavour{'commandFlag'} list-unit-files --no-legend --state=enabled`; + my $res = {}; + foreach my $line (split '\n', $lines) { + chomp $line; + last if $line eq ""; + $line =~ /^(\S+)\s+(\S+)/ or next; + $res->{$1} = 1; + } + return $res; +} + +if ($flavour eq "nixup") { + my $enabledNew = getEnabledUnits; + foreach my $enabledUnit (keys %$enabledNew) { + my $state = `LANG= @systemd@/bin/systemctl $flavour{'commandFlag'} show --property=ActiveState --value $enabledUnit`; + chomp $state; + if ($state eq "inactive") { + print STDERR "starting the following inactive unit: $enabledUnit\n"; + system("@systemd@/bin/systemctl", "$flavour{'commandFlag'}", "start", "--", "$enabledUnit") == 0 or $res = 4; + } + } +} # Print failed and new units. @@ -429,7 +501,7 @@ sub filterUnits { } elsif ($state->{state} eq "auto-restart") { # A unit in auto-restart state is a failure *if* it previously failed to start - my $lines = `@systemd@/bin/systemctl show '$unit'`; + my $lines = `@systemd@/bin/systemctl $flavour{'commandFlag'} show '$unit'`; my $info = {}; parseKeyValues($info, split("\n", $lines)); @@ -449,15 +521,15 @@ sub filterUnits { print STDERR "warning: the following units failed: ", join(", ", sort(@failed)), "\n"; foreach my $unit (@failed) { print STDERR "\n"; - system("COLUMNS=1000 @systemd@/bin/systemctl status --no-pager '$unit' >&2"); + system("COLUMNS=1000 @systemd@/bin/systemctl $flavour{'commandFlag'} status --no-pager '$unit' >&2"); } $res = 4; } if ($res == 0) { - syslog(LOG_NOTICE, "finished switching to system configuration $out"); + syslog(LOG_NOTICE, "finished switching to $flavour configuration $out"); } else { - syslog(LOG_ERR, "switching to system configuration $out failed (status $res)"); + syslog(LOG_ERR, "switching to $flavour configuration $out failed (status $res)"); } exit $res; diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix index 67cb2264e3f37..21a24c03d633b 100644 --- a/nixos/modules/system/activation/top-level.nix +++ b/nixos/modules/system/activation/top-level.nix @@ -126,6 +126,7 @@ let # Needed by switch-to-configuration. perl = "${pkgs.perl}/bin/perl -I${pkgs.perlPackages.FileSlurp}/lib/perl5/site_perl"; + flavour = "nixos"; } else throw "\nFailed assertions:\n${concatStringsSep "\n" (map (x: "- ${x}") failed)}"); # Replace runtime dependencies diff --git a/nixos/modules/virtualisation/container-config.nix b/nixos/modules/virtualisation/container-config.nix index b4f9d8b6fc178..5e368acd6d8b5 100644 --- a/nixos/modules/virtualisation/container-config.nix +++ b/nixos/modules/virtualisation/container-config.nix @@ -11,7 +11,7 @@ with lib; services.udisks2.enable = mkDefault false; powerManagement.enable = mkDefault false; - networking.useHostResolvConf = true; + networking.useHostResolvConf = mkDefault true; # Containers should be light-weight, so start sshd on demand. services.openssh.startWhenNeeded = mkDefault true; diff --git a/nixup/default.nix b/nixup/default.nix new file mode 100644 index 0000000000000..9e45aef261f72 --- /dev/null +++ b/nixup/default.nix @@ -0,0 +1,17 @@ +{ configuration ? import ../nixos/lib/from-env.nix "NIXUP_CONFIG" +, system ? builtins.currentSystem +}: + +let + + eval = import ./lib/eval-config.nix { + modules = [ configuration { nixpkgs.system = system; } ]; + }; + +in + +{ + inherit (eval) config options; + + profile = eval.config.nixup.build.profile; +} diff --git a/nixup/lib/eval-config.nix b/nixup/lib/eval-config.nix new file mode 100644 index 0000000000000..49966c8b0601b --- /dev/null +++ b/nixup/lib/eval-config.nix @@ -0,0 +1,58 @@ +# From an end-user configuration file (`configuration'), build a NixUP +# configuration object (`config') from which we can retrieve option +# values. + +# !!! Please think twice before adding to this argument list! +# Ideally eval-config.nix would be an extremely thin wrapper +# around lib.evalModules, so that modular systems that have nixos configs +# as subcomponents (e.g. the container feature, or nixops if network +# expressions are ever made modular at the top level) can just use +# types.submodule instead of using eval-config.nix +{ # !!! system can be set modularly, would be nice to remove + system ? builtins.currentSystem +, # !!! is this argument needed any more? The pkgs argument can + # be set modularly anyway. + pkgs ? null +, # !!! what do we gain by making this configurable? + baseModules ? import ../modules/module-list.nix +, # !!! See comment about args in lib/modules.nix + extraArgs ? {} +, modules +, # !!! See comment about check in lib/modules.nix + check ? true +, prefix ? [] +, lib ? import +}: + +let extraArgs_ = extraArgs; pkgs_ = pkgs; system_ = system; +in + +let + pkgsModule = rec { + _file = ./eval-config.nix; + key = _file; + config = { + nixpkgs.system = lib.mkDefault system_; + _module.args.pkgs = lib.mkIf (pkgs_ != null) (lib.mkForce pkgs_); + }; + }; + +in rec { + + # Merge the option definitions in all modules, forming the full + # system configuration. + inherit (lib.evalModules { + inherit prefix check; + modules = modules ++ baseModules ++ [ pkgsModule ]; + args = extraArgs; + specialArgs = { modulesPath = ../modules; }; + }) config options; + + # These are the extra arguments passed to every module. In + # particular, Nixpkgs is passed through the "pkgs" argument. + extraArgs = extraArgs_ // { + inherit modules baseModules; + }; + + inherit (config._module.args) pkgs; +} diff --git a/nixup/modules/config/nix.nix b/nixup/modules/config/nix.nix new file mode 100644 index 0000000000000..98967b3b50377 --- /dev/null +++ b/nixup/modules/config/nix.nix @@ -0,0 +1,20 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + + options = { + + nix.package = mkOption { + type = types.package; + default = pkgs.nix; + description = '' + This option specifies the Nix package instance to use + within the nixuser-rebuild command. + ''; + }; + + }; + +} diff --git a/nixup/modules/config/packages.nix b/nixup/modules/config/packages.nix new file mode 100644 index 0000000000000..1e9f7d4e0b681 --- /dev/null +++ b/nixup/modules/config/packages.nix @@ -0,0 +1,79 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + userPath = pkgs.buildEnv { + name = "user-path"; + paths = config.user.packages; + pathsToLink = config.user.pathsToLink; + ignoreCollisions = true; + # !!! Hacky, should modularise. + postBuild = + '' + if [ -x $out/bin/update-mime-database -a -w $out/share/mime/packages ]; then + XDG_DATA_DIRS=$out/share $out/bin/update-mime-database -V $out/share/mime > /dev/null + fi + + if [ -x $out/bin/gtk-update-icon-cache -a -f $out/share/icons/hicolor/index.theme ]; then + $out/bin/gtk-update-icon-cache $out/share/icons/hicolor + fi + + if [ -x $out/bin/glib-compile-schemas -a -w $out/share/glib-2.0/schemas ]; then + $out/bin/glib-compile-schemas $out/share/glib-2.0/schemas + fi + + if [ -x $out/bin/update-desktop-database -a -w $out/share/applications ]; then + $out/bin/update-desktop-database $out/share/applications + fi + ''; + }; + +in + +{ + + options = { + + user.packages = mkOption { + default = []; + type = types.listOf types.path; + example = literalExample "with config.nixpkgs.pkgs; [ firefox thunderbird ]"; + description = '' + The set of packages that appear in + $XDG_RUNTIME_DIR/nixup/active-profile/sw. These packages are + automatically updated every time you rebuild the user profile. + ''; + }; + + user.pathsToLink = mkOption { + type = types.listOf types.str; + default = [ + "/bin" + "/etc/xdg" + "/info" + "/man" + "/sbin" + "/share/emacs" + "/share/vim-plugins" + "/share/org" + "/share/info" + "/share/terminfo" + "/share/man" + ]; + example = ["/"]; + description = '' + List of directories to be symlinked in $XDG_RUNTIME_DIR/nixup/active-profile/sw. + ''; + }; + + }; + + config = { + nixup.build.userPath = userPath; + nixup.buildCommands = '' + ln -s ${userPath} $out/sw + ''; + }; +} diff --git a/nixup/modules/misc/legacy-nixpkgs-config.nix b/nixup/modules/misc/legacy-nixpkgs-config.nix new file mode 100644 index 0000000000000..462c8ce545179 --- /dev/null +++ b/nixup/modules/misc/legacy-nixpkgs-config.nix @@ -0,0 +1,61 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.nixpkgs; + + + # The contents of the configuration file found at $NIXPKGS_CONFIG or + # $HOME/.nixpkgs/config.nix. + # for NIXOS (nixos-rebuild): use nixpkgs.config option + nixpkgs_config = + let + toPath = builtins.toPath; + getEnv = x: if builtins ? getEnv then builtins.getEnv x else ""; + pathExists = name: + builtins ? pathExists && builtins.pathExists (toPath name); + + configFile = getEnv "NIXPKGS_CONFIG"; + homeDir = getEnv "HOME"; + configFile2 = homeDir + "/.nixpkgs/config.nix"; + + configExpr = + if configFile != "" && pathExists configFile then import (toPath configFile) + else if homeDir != "" && pathExists configFile2 then import (toPath configFile2) + else {}; + + in + # allow both: + # { /* the config */ } and + # { pkgs, ... } : { /* the config */ } + if builtins.isFunction configExpr + then configExpr { inherit pkgs; } + else configExpr; + +in + +{ + options = { + + nixpkgs.includeLegacyConfig = mkOption { + default = true; + type = types.bool; + description = '' + Merge legacy nixpkgs configuration from "~/.nixpkgs/config.nix" + into the "nixpkgs.config" option. + NOTE: The option will default to "false" after a transition period. + So move your configuration from "~/.nixpkgs/config.nix" into + "$XDG_CONFIG_HOME/nixup/profile.nix" under the "nixpkgs.config" option. + ''; + }; + + }; + + config = { + + nixpkgs.config = mkIf cfg.includeLegacyConfig nixpkgs_config; + + }; +} diff --git a/nixup/modules/module-list.nix b/nixup/modules/module-list.nix new file mode 100644 index 0000000000000..d7dfba0a063f6 --- /dev/null +++ b/nixup/modules/module-list.nix @@ -0,0 +1,12 @@ +[ ./nixpkgs.nix + ./config/nix.nix + ./config/packages.nix + ../../nixos/modules/misc/assertions.nix + ./misc/legacy-nixpkgs-config.nix + ./nixup/activation.nix + ./nixup/build.nix + ./nixup/nixup-tools.nix + ./nixup/resource-files.nix + ./nixup/switch-to-configuration.nix + ./nixup/systemd.nix +] diff --git a/nixup/modules/nixpkgs.nix b/nixup/modules/nixpkgs.nix new file mode 100644 index 0000000000000..b48486003d5ea --- /dev/null +++ b/nixup/modules/nixpkgs.nix @@ -0,0 +1,92 @@ +{ config, lib, ... }: + +with lib; + +let + + isConfig = x: + builtins.isAttrs x || builtins.isFunction x; + + optCall = f: x: + if builtins.isFunction f + then f x + else f; + + mergeConfig = lhs_: rhs_: + let + lhs = optCall lhs_ { inherit pkgs; }; + rhs = optCall rhs_ { inherit pkgs; }; + in + lhs // rhs // + optionalAttrs (lhs ? packageOverrides) { + packageOverrides = pkgs: + optCall lhs.packageOverrides pkgs // + optCall (attrByPath ["packageOverrides"] ({}) rhs) pkgs; + }; + + configType = mkOptionType { + name = "nixpkgs config"; + check = traceValIfNot isConfig; + merge = args: fold (def: mergeConfig def.value) {}; + }; + +in + +{ + options = { + nixpkgs.path = mkOption { + type = types.path; + example = "/home/user/dev/nixpkgs"; + description = '' + Location of the collection of packages which is used for building + the current user profile. This is also useful for building profile + against either bleeding-edge recipes or archived version of the + recipes. + ''; + }; + + nixpkgs.config = mkOption { + default = {}; + example = literalExample + '' + { firefox.enableGeckoMediaPlayer = true; + packageOverrides = pkgs: { + firefox60Pkgs = pkgs.firefox60Pkgs.override { + enableOfficialBranding = true; + }; + }; + } + ''; + type = configType; + description = '' + The configuration of the Nix Packages collection. (For + details, see the Nixpkgs documentation.) It allows you to set + package configuration options, and to override packages + globally through the packageOverrides + option. The latter is a function that takes as an argument + the original Nixpkgs, and must evaluate + to a set of new or overridden packages. + ''; + }; + + nixpkgs.system = mkOption { + type = types.str; + example = "i686-linux"; + description = '' + Specifies the Nix platform type for which NixOS should be built. + If unset, it defaults to the platform type of your host system. + Specifying this option is useful when doing distributed + multi-platform deployment, or when building virtual machines. + ''; + }; + + }; + + config = { + nixpkgs.path = ; + + _module.args.pkgs = import config.nixpkgs.path { + inherit (config.nixpkgs) system config; + }; + }; +} diff --git a/nixup/modules/nixup/activation.nix b/nixup/modules/nixup/activation.nix new file mode 100644 index 0000000000000..7226077bc6183 --- /dev/null +++ b/nixup/modules/nixup/activation.nix @@ -0,0 +1,128 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + addAttributeName = mapAttrs (a: v: v // { + text = '' + #### Activation script snippet ${a}: + ${v.text} + ''; + }); + + path = [ pkgs.coreutils ]; + + activateScript = pkgs.writeScript "user-profile-activate" config.nixup.activationScripts; + +in + +{ + + options = { + + nixup.activationScripts = mkOption { + default = {}; + + example = { + aliasIfconfig = { + text = '' + # Some local configuration + if ifconfig > /dev/null 2>&1; then + : + elif test -x /sbin/ifconfig; then + ln -s /sbin/ifconfig ~/.usr/bin/ifconfig + fi + ''; + deps = [ "setupLocalUsr" ]; + }; + }; + + description = '' + A set of shell script fragments that are executed when a NixUP + user configuration is activated. Since these + are executed every time you login or run + nixup-rebuild, it's important that they are + idempotent and fast. + ''; + + type = types.attrsOf types.unspecified; # FIXME + + apply = set: + '' + #! ${pkgs.stdenv.shell} + + if [ -n "''${NIXUP_RUNTIME_DIR:?'NIXUP_RUNTIME_DIR not set'}" ]; then + echo "Activating NixUP user environment..." + fi + + export PATH=/empty + for i in ${toString path}; do + PATH=$PATH:$i/bin:$i/sbin + done + + _status=0 + trap "_status=1" ERR + + # Ensure a consistent umask. + umask 0022 + + # NixUP runtime directory. + mkdir -m 0755 -p $NIXUP_RUNTIME_DIR + + # NixUP garbage collector roots directory. + export NIXUP_USER_GCROOTS_DIR=/nix/var/nix/gcroots/nixup/$USER + + # Clean up a previously failed activation + rm -f $NIXUP_RUNTIME_DIR/old-active-profile + rm -f $NIXUP_USER_GCROOTS_DIR/old-active-profile + + # Move currently active profile out of the way + if [ -e $NIXUP_RUNTIME_DIR/active-profile ]; then + cp -d $NIXUP_RUNTIME_DIR/active-profile $NIXUP_RUNTIME_DIR/old-active-profile + fi + if [ -e $NIXUP_USER_GCROOTS_DIR/active-profile ]; then + mv $NIXUP_USER_GCROOTS_DIR/active-profile $NIXUP_USER_GCROOTS_DIR/old-active-profile + fi + + # Prevent the current configuration from being garbage-collected. + ln -sfn @out@ $NIXUP_USER_GCROOTS_DIR/active-profile + + # Set new active profile. + ln -sfn @out@ $NIXUP_RUNTIME_DIR/active-profile + + ${ + let + set' = mapAttrs (n: v: if isString v then noDepEntry v else v) set; + withHeadlines = addAttributeName set'; + in textClosureMap id (withHeadlines) (attrNames withHeadlines) + } + + # Mark old configuration for garbage collection. + rm -f $NIXUP_USER_GCROOTS_DIR/old-active-profile + + # Allow to keep old profile for later use in, e.g., switch-to-configuration.pl + if [ -z $NIXUP_ACTIVATE_NO_CLEANUP ]; then + # Remove old configuration. + rm -f $NIXUP_RUNTIME_DIR/old-active-profile + fi + + # Make his configuration the current configuration. + ln -sfn "$NIXUP_RUNTIME_DIR/active-profile/sw" $HOME/.nix-profile + + exit $_status + ''; + + }; + + }; + + config = { + nixup.buildCommands = '' + cp ${toString activateScript} $out/activate + substituteInPlace $out/activate --subst-var out + chmod u+x $out/activate + unset activationScript + ''; + }; +} diff --git a/nixup/modules/nixup/build.nix b/nixup/modules/nixup/build.nix new file mode 100644 index 0000000000000..0e644f8e4f6cc --- /dev/null +++ b/nixup/modules/nixup/build.nix @@ -0,0 +1,49 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + profile = pkgs.stdenv.mkDerivation { + name = "nixup-profile"; + preferLocalBuild = true; + buildCommand = '' + mkdir $out + + ${config.nixup.buildCommands} + ''; + }; + +in + +{ + options = { + nixup.build = mkOption { + internal = true; + type = types.attrsOf types.package; + default = {}; + description = '' + Attribute set of derivations used to setup the system. + ''; + }; + + nixup.buildCommands = mkOption { + internal = true; + type = types.lines; + default = []; + example = literalExample '' + "ln -s ${pkgs.firefox} $out/firefox-stable" + ''; + description = '' + List of commands to build and install the content of the profile + directory. + ''; + }; + }; + + config = { + + nixup.build.profile = profile; + + }; +} diff --git a/nixup/modules/nixup/make-managed.sh b/nixup/modules/nixup/make-managed.sh new file mode 100644 index 0000000000000..4f7dbbfdd1804 --- /dev/null +++ b/nixup/modules/nixup/make-managed.sh @@ -0,0 +1,66 @@ +source $stdenv/setup + +mkdir -p $out/sources +mkdir -p $out/dynamics +mkdir -p $out/volatiles +mkdir -p $out/modes +mkdir -p $out/uids +mkdir -p $out/gids + +set -f +targets_=($targets) +sources_=($sources) +dynamics_=($dynamics) +volatiles_=($volatiles) +modes_=($modes) +users_=($users) +groups_=($groups) +set +f + +for ((i = 0; i < ${#targets_[@]}; i++)); do + source="${sources_[$i]}" + target="${targets_[$i]}" + + if [[ "$source" =~ '*' ]]; then + + # If the source name contains '*', perform globbing. + mkdir -p $out/sources/$target + for fn in $source; do + ln -s "$fn" $out/sources/$target/ + done + + else + + mkdir -p $out/sources/$(dirname $target) + if ! [ -e $out/sources/$target ]; then + ln -s $source $out/sources/$target + else + echo "duplicate entry $target -> $source" + if test "$(readlink $out/sources/$target)" != "$source"; then + echo "mismatched duplicate entry $(readlink $out/sources/$target) <-> $source" + exit 1 + fi + fi + + if test "${dynamics_[$i]}" == true; then + mkdir -p $out/dynamics/$(dirname $target) + touch $out/dynamics/$target + fi + + if test "${volatiles_[$i]}" == true; then + mkdir -p $out/volatiles/$(dirname $target) + touch $out/volatiles/$target + fi + + if test "${modes_[$i]}" != symlink; then + mkdir -p $out/modes/$(dirname $target) + echo "${modes_[$i]}" > $out/modes/$target + mkdir -p $out/uids/$(dirname $target) + echo "${users_[$i]}" > $out/uids/$target + mkdir -p $out/gids/$(dirname $target) + echo "${groups_[$i]}" > $out/gids/$target + fi + + fi +done + diff --git a/nixup/modules/nixup/nixup-tools.nix b/nixup/modules/nixup/nixup-tools.nix new file mode 100644 index 0000000000000..c34c3d572e927 --- /dev/null +++ b/nixup/modules/nixup/nixup-tools.nix @@ -0,0 +1,53 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.nixup-tools; + + managed-packages = + let + packagesString = if cfg.managed-packages-path == null + then let xdg_config_home_ = builtins.getEnv "XDG_CONFIG_HOME"; + xdg_config_home = if xdg_config_home_ == "" then "${builtins.getEnv "HOME"}/.config" else xdg_config_home_; + in + if builtins.pathExists "${xdg_config_home}/nixup/packages.nix" + then readFile (builtins.toPath "${xdg_config_home}/nixup/packages.nix") + else "" + else if builtins.pathExists (builtins.toPath cfg.managed-packages-path) + then readFile (builtins.toPath cfg.managed-packages-path) + else ""; + in + map (x : getAttr x pkgs) (remove "" (map (x: replaceChars ["\n"] [""] x) (splitString "\n" packagesString))); + +in + +{ + options = { + + nixup-tools = { + enable = mkOption { + default = false; + type = types.bool; + description = '' + Enable special tooling for NixUP. + ''; + }; + managed-packages-path = mkOption { + default = null; + type = types.nullOr types.path; + description = '' + Path of file to include into "user.packages". + "null" uses "$XDG_CONFIG_HOME/nixup/packages.nix". + ''; + }; + }; + }; + + config = { + + user.packages = mkIf cfg.enable ([ pkgs.nixup-tools ] ++ managed-packages); + + }; +} diff --git a/nixup/modules/nixup/resource-files.nix b/nixup/modules/nixup/resource-files.nix new file mode 100644 index 0000000000000..2fc3c1b9d25c8 --- /dev/null +++ b/nixup/modules/nixup/resource-files.nix @@ -0,0 +1,200 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + managed' = filter (f: f.enable) (attrValues config.user.files); + + managed = pkgs.stdenvNoCC.mkDerivation { + name = "managed"; + + builder = ./make-managed.sh; + + preferLocalBuild = true; + allowSubstitutes = false; + + /* !!! Use toXML. */ + targets = map (x: x.target) managed'; + sources = map (x: x.source) managed'; + dynamics = map (x: x.dynamic) managed'; + volatiles = map (x: x.volatile) managed'; + modes = map (x: x.mode) managed'; + users = map (x: x.user) managed'; + groups = map (x: x.group) managed'; + }; +in + +{ + options = { + user.files = mkOption rec { + default = {}; + type = types.loaOf (types.submodule options); + example = literalExample '' + [ { target = ".ssh/config"; + content = " + Host home.my-domain.name + User seti + "; + } + { target = ".gitconfig"; + source = ./gitconfig; + } + ] + ''; + description = '' + Set of files that have to be linked in the home directory. + ''; + + options = { name, config, ... }: { + options = { + enable = mkOption { + type = types.bool; + default = true; + description = '' + Whether this ~/. file should be generated. This + option allows specific ~/. files to be disabled. + ''; + }; + + target = mkOption { + type = types.relativePath; + description = '' + Name of symlink (relative to ~/.). + Defaults to the attribute name. + ''; + }; + + dynamic = mkOption { + type = types.bool; + default = false; + description = '' + WARNING: USE WITH CARE!! + + Execute the file instead of linking or copying it. + + The file is expected to be an executable that receives the "target" filename + as its only input parameter and then generates the "target" file dynamically. + So it is possible for the executable to alter its behaviour depending on the + current "target" file and e.g. initialize, upgrade or reset an existing file. + The executable is re-executed with every run of the top-level activation script + and is expected to behave as idempotent as sensible. + ''; + }; + + volatile = mkOption { + type = types.bool; + default = false; + description = '' + Expect the content of the file to change after being generated. + That option disables warnings if the managed file is modified directly. + ''; + }; + + content = mkOption { + default = null; + type = types.nullOr types.lines; + description = "Content of the file."; + }; + + source = mkOption { + type = types.path; + description = "Path of the source file."; + }; + + mode = mkOption { + type = types.str; + default = "symlink"; + example = "0600"; + description = '' + If set to something else than symlink, + the file is copied instead of symlinked, with the given + file mode. + ''; + }; + + uid = mkOption { + default = 0; + type = types.int; + description = '' + UID of created file. Only takes affect when the file is + copied (that is, the mode is not 'symlink'). + ''; + }; + + gid = mkOption { + default = 0; + type = types.int; + description = '' + GID of created file. Only takes affect when the file is + copied (that is, the mode is not 'symlink'). + ''; + }; + + user = mkOption { + default = "+${toString config.uid}"; + type = types.str; + description = '' + User name of created file. + Only takes affect when the file is copied (that is, the mode is not 'symlink'). + Changing this option takes precedence over uid. + ''; + }; + + group = mkOption { + default = "+${toString config.gid}"; + type = types.str; + description = '' + Group name of created file. + Only takes affect when the file is copied (that is, the mode is not 'symlink'). + Changing this option takes precedence over gid. + ''; + }; + + }; + + config = { + target = mkDefault name; + source = mkIf (config.content != null) + (mkDefault (pkgs.writeText "managed-${name}" config.content)); + }; + }; + + }; + }; + + config = { + nixup.buildCommands = '' + mkdir -p $out + ln -s ${managed} $out/resources + + ## + ## create resource linking/unlinking/switching commands + ## NOTE: The commands link directly to the derivation of the profile, and not to $NIXUP_RUNTIME_DIR/active-profile! + ## + mkdir -p $out/bin + + echo "#! ${pkgs.stdenv.shell}" > $out/bin/nixup-resources-link + echo "/run/wrappers/bin/resource-manager link \$HOME ${managed}" >> $out/bin/nixup-resources-link + chmod +x $out/bin/nixup-resources-link + + echo "#! ${pkgs.stdenv.shell}" > $out/bin/nixup-resources-unlink + echo "/run/wrappers/bin/resource-manager unlink \$HOME ${managed}" >> $out/bin/nixup-resources-unlink + chmod +x $out/bin/nixup-resources-unlink + + echo "#! ${pkgs.stdenv.shell}" > $out/bin/nixup-resources-cleanup + echo "/run/wrappers/bin/resource-manager cleanup \$HOME" >> $out/bin/nixup-resources-cleanup + chmod +x $out/bin/nixup-resources-cleanup + ''; + + nixup.activationScripts.resourceFiles = '' + echo "Setting up resource files ..." + if [ -L "$NIXUP_USER_GCROOTS_DIR/old-active-profile" ]; + then + /run/wrappers/bin/resource-manager switch $HOME `${pkgs.coreutils}/bin/readlink $NIXUP_USER_GCROOTS_DIR/old-active-profile/resources` ${managed} + else + #TODO: implement "cleanup"# /run/wrappers/bin/resource-manager cleanup $HOME ${managed} + /run/wrappers/bin/resource-manager link $HOME ${managed} + fi + ''; + }; +} diff --git a/nixup/modules/nixup/switch-to-configuration.nix b/nixup/modules/nixup/switch-to-configuration.nix new file mode 100644 index 0000000000000..01351ea16c7b2 --- /dev/null +++ b/nixup/modules/nixup/switch-to-configuration.nix @@ -0,0 +1,31 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + + config = { + + nixup.buildCommands = '' + mkdir -p $out/bin + export perl="${pkgs.perl}/bin/perl -I${pkgs.perlPackages.FileSlurp}/lib/perl5/site_perl"; + export coreutils=${pkgs.coreutils} + export systemd=${config.systemd.package} + export flavour='nixup' + export installBootLoader='/dev/null' + export utillinux='/dev/null' + substituteAll ${../../../nixos/modules/system/activation/switch-to-configuration.pl} $out/bin/switch-to-configuration + chmod +x $out/bin/switch-to-configuration + ''; + + nixup.activationScripts.resourceFiles = '' + echo "Setting up systemd user services ..." + mkdir -p ''${XDG_RUNTIME_DIR:-/run/user/$(id -u $USER)}/systemd + if [ ! -L "''${XDG_RUNTIME_DIR:-/run/user/$(id -u $USER)}/systemd/user" ]; then + ln -sf $NIXUP_RUNTIME_DIR/active-profile/systemd ''${XDG_RUNTIME_DIR:-/run/user/$(id -u $USER)}/systemd/user + fi + ''; + + }; + +} diff --git a/nixup/modules/nixup/systemd.nix b/nixup/modules/nixup/systemd.nix new file mode 100644 index 0000000000000..c5c58fe66bc18 --- /dev/null +++ b/nixup/modules/nixup/systemd.nix @@ -0,0 +1,29 @@ +{ config, lib, pkgs, utils, ... }: + +let + config_ = { + systemd = config.systemd // { user = config.user; }; + }; + + wrapper = import ../../../nixos/modules/system/boot/systemd.nix { config=config_; inherit lib pkgs utils; }; + + systemd-lib = import ../../../nixos/modules/system/boot/systemd-lib.nix { config=config_; inherit lib pkgs; }; +in + +{ + options = { + systemd = { inherit (wrapper.options.systemd) package packages defaultUnit ctrlAltDelUnit globalEnvironment; }; + + user = wrapper.options.systemd.user; + }; + + config = { + nixup.buildCommands = '' + ln -s ${config.nixup.build.systemd} $out/systemd + ''; + + nixup.build.systemd = systemd-lib.generateUnits "user" config.user.units [] []; + + user = { inherit (wrapper.config.systemd.user) units timers; }; + }; +} diff --git a/pkgs/applications/misc/nixup-tools/default.nix b/pkgs/applications/misc/nixup-tools/default.nix new file mode 100644 index 0000000000000..ca509b5178a47 --- /dev/null +++ b/pkgs/applications/misc/nixup-tools/default.nix @@ -0,0 +1,17 @@ +{ stdenv, coreutils, gnused, nix }: + +stdenv.mkDerivation rec { + name = "nixup-${version}"; + version = "0.0.1"; + + buildCommand = '' + mkdir -p $out/bin + shell=${stdenv.shell} + export coreutils=${coreutils} + export gnused=${gnused} + export nix=${nix} + substituteAll ${./nixup.sh} $out/bin/nixup + chmod +x $out/bin/nixup + ''; +} + diff --git a/pkgs/applications/misc/nixup-tools/nixup.sh b/pkgs/applications/misc/nixup-tools/nixup.sh new file mode 100644 index 0000000000000..2fa67bbbea565 --- /dev/null +++ b/pkgs/applications/misc/nixup-tools/nixup.sh @@ -0,0 +1,84 @@ +#! @shell@ + +CONFIG_FILE=${XDG_CONFIG_HOME:-$HOME/.config}/nixup/packages.nix +PACKAGE_NAME=$package + +ACTION= + +while [[ "$#" -gt 0 ]]; do + i="$1"; shift 1 + case "$i" in + package) + j="$1"; shift 1 + case "$j" in + install|remove) + ACTION="$i"-"$j" + PACKAGE_NAME="$1"; shift 1 + ;; + *) + echo "$0: unknown option \`$j'" + exit 1 + ;; + esac + ;; + switch|build) + ACTION="$i" + ;; + *) + echo "$0: unknown command \`$i'" + exit 1 + ;; + esac +done + +if [ "$ACTION" = "switch" ]; then + ${NIXUP_API:?}/bin/nixup-rebuild switch +fi + +if [ "$ACTION" = "build" ]; then + ${NIXUP_API:?}/bin/nixup-rebuild build +fi + +if [ "$ACTION" = "package-remove" ]; then + if [[ -e $CONFIG_FILE ]]; then + @gnused@/bin/sed -n "/^$PACKAGE_NAME\$/q 1" "$CONFIG_FILE" + if [[ $? == 1 ]]; then + echo "Removing $PACKAGE_NAME..." + @gnused@/bin/sed -i "/^$PACKAGE_NAME\$/d" "$CONFIG_FILE" + @gnused@/bin/sed -i '/^[[:space:]]*$/d' "$CONFIG_FILE" + ${NIXUP_API:?}/bin/nixup-rebuild switch + else + echo "ERROR: $PACKAGE_NAME is not installed!" + exit 1 + fi + else + echo "ERROR: $PACKAGE_NAME is not installed!" + exit 1 + fi +fi + + +if [[ "$ACTION" = "package-install" ]]; then + isAvailable=$(@nix@/bin/nix-instantiate --eval -E "builtins.hasAttr \"$PACKAGE_NAME\" (import {})") + if [[ "$isAvailable" == "false" ]]; then + echo "ERROR: Package is not available." + exit 1 + fi + if [[ -e $CONFIG_FILE ]]; then + @gnused@/bin/sed -n "/^$PACKAGE_NAME\$/q 1" "$CONFIG_FILE" + if [ $? == 0 ]; then + echo "Installing $PACKAGE_NAME..." + echo "$PACKAGE_NAME" >> "$CONFIG_FILE" + @gnused@/bin/sed -i '/^[[:space:]]*$/d' "$CONFIG_FILE" + ${NIXUP_API:?}/bin/nixup-rebuild switch + else + echo "WARNING: $PACKAGE_NAME is already installed!" + exit 1 + fi + else + @coreutils@/bin/mkdir -p "$(dirname "$CONFIG_FILE")" + echo "Installing $PACKAGE_NAME..." + echo "$PACKAGE_NAME" > "$CONFIG_FILE" + ${NIXUP_API:?}/bin/nixup-rebuild switch + fi +fi diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 13d168b1f8c04..bddf86d863755 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -3056,6 +3056,8 @@ with pkgs; ninka = callPackage ../development/tools/misc/ninka { }; + nixup-tools = callPackage ../applications/misc/nixup-tools { }; + nodejs = hiPrio nodejs-6_x; nodejs-slim = nodejs-slim-6_x;