diff --git a/home-manager/default.nix b/home-manager/default.nix index 943d61e81..394f0e7f2 100644 --- a/home-manager/default.nix +++ b/home-manager/default.nix @@ -15,9 +15,7 @@ in pkgs.stdenv.mkDerivation { name = "home-manager"; - phases = [ "installPhase" ]; - - installPhase = '' + buildCommand = '' install -v -D -m755 ${./home-manager} $out/bin/home-manager substituteInPlace $out/bin/home-manager \ diff --git a/modules/files.nix b/modules/files.nix index e294f97f0..c61560c5b 100644 --- a/modules/files.nix +++ b/modules/files.nix @@ -6,8 +6,17 @@ with import ./lib/dag.nix { inherit lib; }; let cfg = config.home.file; + homeDirectory = config.home.homeDirectory; + fileType = (import lib/file-type.nix { + inherit homeDirectory lib pkgs; + }).fileType; + + # A symbolic link whose target path matches this pattern will be + # considered part of a Home Manager generation. + homeFilePattern = "${builtins.storeDir}/*-home-manager-files/*"; + in { @@ -15,50 +24,7 @@ in home.file = mkOption { description = "Attribute set of files to link into the user home."; default = {}; - type = types.loaOf (types.submodule ( - { name, config, ... }: { - options = { - target = mkOption { - type = types.str; - apply = removePrefix (homeDirectory + "/"); - description = '' - Path to target file relative to HOME. - ''; - }; - - text = mkOption { - default = null; - type = types.nullOr types.lines; - description = "Text of the file."; - }; - - source = mkOption { - type = types.path; - description = '' - Path of the source file. The file name must not start - with a period since Nix will not allow such names in - the Nix store. - - This may refer to a directory. - ''; - }; - - mode = mkOption { - type = types.str; - default = "444"; - description = "The permissions to apply to the file."; - }; - }; - - config = { - target = mkDefault name; - source = mkIf (config.text != null) ( - let name' = "user-etc-" + baseNameOf name; - in mkDefault (pkgs.writeText name' config.text) - ); - }; - }) - ); + type = fileType "HOME" homeDirectory; }; home-files = mkOption { @@ -83,11 +49,23 @@ in }) ]; + warnings = + let + badFiles = + map (f: f.target) + (filter (f: f.mode != null) + (attrValues cfg)); + badFilesStr = toString badFiles; + in + mkIf (badFiles != []) [ + ("The 'mode' field is deprecated for 'home.file', " + + "use 'executable' instead: ${badFilesStr}") + ]; + # This verifies that the links we are about to create will not # overwrite an existing file. home.activation.checkLinkTargets = dagEntryBefore ["writeBoundary"] ( let - pattern = "-home-manager-files/"; check = pkgs.writeText "check" '' . ${./lib-bash/color-echo.sh} @@ -97,7 +75,7 @@ in relativePath="''${sourcePath#$newGenFiles/}" targetPath="$HOME/$relativePath" if [[ -e "$targetPath" \ - && ! "$(readlink "$targetPath")" =~ "${pattern}" ]] ; then + && ! "$(readlink "$targetPath")" == ${homeFilePattern} ]] ; then errorEcho "Existing file '$targetPath' is in the way" collision=1 fi @@ -123,8 +101,6 @@ in home.activation.linkGeneration = dagEntryAfter ["writeBoundary"] ( let - pattern = "-home-manager-files/"; - link = pkgs.writeText "link" '' newGenFiles="$1" shift @@ -147,7 +123,7 @@ in targetPath="$HOME/$relativePath" if [[ -e "$newGenFiles/$relativePath" ]] ; then $VERBOSE_ECHO "Checking $targetPath: exists" - elif [[ ! "$(readlink "$targetPath")" =~ "${pattern}" ]] ; then + elif [[ ! "$(readlink "$targetPath")" == ${homeFilePattern} ]] ; then warnEcho "Path '$targetPath' not link into Home Manager generation. Skipping delete." else $VERBOSE_ECHO "Checking $targetPath: gone (deleting)" @@ -210,30 +186,57 @@ in home-files = pkgs.stdenv.mkDerivation { name = "home-manager-files"; - phases = [ "installPhase" ]; + # Symlink directories and files that have the right execute bit. + # Copy files that need their execute bit changed or use the + # deprecated 'mode' option. + buildCommand = '' + mkdir -p $out - installPhase = - "mkdir -p $out\n" + - concatStringsSep "\n" ( - mapAttrsToList (n: v: - '' - target="$(realpath -m "$out/${v.target}")" + function insertFile() { + local source="$1" + local relTarget="$2" + local executable="$3" + local mode="$4" # For backwards compatibility. - # Target file must be within $HOME. - if [[ ! "$target" =~ "$out" ]] ; then - echo "Error installing file '${v.target}' outside \$HOME" >&2 - exit 1 - fi + # Figure out the real absolute path to the target. + local target + target="$(realpath -m "$out/$relTarget")" - if [ -d "${v.source}" ]; then - mkdir -p "$(dirname "$out/${v.target}")" - ln -s "${v.source}" "$target" + # Target path must be within $HOME. + if [[ ! $target =~ $out ]] ; then + echo "Error installing file '$relTarget' outside \$HOME" >&2 + exit 1 + fi + + mkdir -p "$(dirname "$target")" + if [[ -d $source ]]; then + ln -s "$source" "$target" + elif [[ $mode ]]; then + install -m "$mode" "$source" "$target" + else + [[ -x $source ]] && isExecutable=1 || isExecutable="" + if [[ $executable == symlink || $isExecutable == $executable ]]; then + ln -s "$source" "$target" + else + cp "$source" "$target" + if [[ $executable ]]; then + chmod +x "$target" else - install -D -m${v.mode} "${v.source}" "$target" + chmod -x "$target" fi - '' - ) cfg - ); + fi + fi + } + '' + concatStrings ( + mapAttrsToList (n: v: '' + insertFile "${v.source}" \ + "${v.target}" \ + "${if v.executable == null + then "symlink" + else builtins.toString v.executable}" \ + "${builtins.toString v.mode}" + '') cfg + ); }; }; } diff --git a/modules/home-environment.nix b/modules/home-environment.nix index 892c20cc8..323294a37 100644 --- a/modules/home-environment.nix +++ b/modules/home-environment.nix @@ -265,7 +265,7 @@ in pkgs.nix ]; - sf = pkgs.writeText "activation-script" '' + activationScript = pkgs.writeScript "activation-script" '' #!${pkgs.stdenv.shell} set -eu @@ -283,10 +283,10 @@ in pkgs.stdenv.mkDerivation { name = "home-manager-generation"; - phases = [ "installPhase" ]; + buildCommand = '' + mkdir -p $out - installPhase = '' - install -D -m755 ${sf} $out/activate + cp ${activationScript} $out/activate substituteInPlace $out/activate \ --subst-var-by GENERATION_DIR $out diff --git a/modules/lib/file-type.nix b/modules/lib/file-type.nix new file mode 100644 index 000000000..ebdcb7741 --- /dev/null +++ b/modules/lib/file-type.nix @@ -0,0 +1,105 @@ +{ homeDirectory, lib, pkgs }: + +with lib; + +let + + # Figures out a valid Nix store name for the given path. + storeFileName = path: + let + # All characters that are considered safe. Note "-" is not + # included to avoid "-" followed by digit being interpreted as a + # version. + safeChars = + [ "+" "." "_" "?" "=" ] + ++ lowerChars + ++ upperChars + ++ stringToCharacters "0123456789"; + + empties = l: genList (x: "") (length l); + + unsafeInName = stringToCharacters ( + replaceStrings safeChars (empties safeChars) path + ); + + safeName = replaceStrings unsafeInName (empties unsafeInName) path; + in + "home_file_" + safeName; + +in + +{ + # Constructs a type suitable for a `home.file` like option. The + # target path may be either absolute or relative, in which case it + # is relative the `basePath` argument (which itself must be an + # absolute path). + # + # Arguments: + # - basePathDesc docbook compatible description of the base path + # - basePath the file base path + fileType = basePathDesc: basePath: types.loaOf (types.submodule ( + { name, config, ... }: { + options = { + target = mkOption { + type = types.str; + apply = p: + let + absPath = if hasPrefix "/" p then p else "${basePath}/${p}"; + in + removePrefix (homeDirectory + "/") absPath; + description = '' + Path to target file relative to ${basePathDesc}. + ''; + }; + + text = mkOption { + default = null; + type = types.nullOr types.lines; + description = "Text of the file."; + }; + + source = mkOption { + type = types.path; + description = '' + Path of the source file. The file name must not start + with a period since Nix will not allow such names in + the Nix store. + + This may refer to a directory. + ''; + }; + + mode = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The permissions to apply to the file. + + DEPRECATED: use home.file.<name?>.executable + instead. + ''; + }; + + executable = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + Set the execute bit. If null, defaults to the mode + of the source file or to false + for files created through the text option. + ''; + }; + }; + + config = { + target = mkDefault name; + source = mkIf (config.text != null) ( + mkDefault (pkgs.writeTextFile { + inherit (config) executable text; + name = storeFileName name; + }) + ); + }; + } + )); +} diff --git a/modules/misc/news.nix b/modules/misc/news.nix index ac969f160..44b9d7f59 100644 --- a/modules/misc/news.nix +++ b/modules/misc/news.nix @@ -425,6 +425,23 @@ in A new window manager module is available: 'xsession.windowManager.i3'. ''; } + + { + time = "2017-11-06T13:23:17+00:00"; + condition = any (f: f.mode != null) (attrValues config.home.file); + message = '' + The + + home.file..mode + + option is now deprecated. Please use + + home.file..executable + + instead. The 'mode' option will be completely removed + December 6, 2017. + ''; + } ]; }; } diff --git a/modules/misc/xdg.nix b/modules/misc/xdg.nix index 8dcf1fba4..8f47ac5dc 100644 --- a/modules/misc/xdg.nix +++ b/modules/misc/xdg.nix @@ -6,55 +6,10 @@ let cfg = config.xdg; - fileType = basePathDesc: basePath: (types.loaOf (types.submodule ( - { name, config, ... }: { - options = { - target = mkOption { - type = types.str; - apply = p: "${basePath}/${p}"; - description = '' - Path to target file relative to ${basePathDesc}. - ''; - }; - - text = mkOption { - default = null; - type = types.nullOr types.lines; - description = "Text of the file."; - }; - - source = mkOption { - type = types.path; - description = '' - Path of the source file. The file name must not start - with a period since Nix will not allow such names in - the Nix store. - - This may refer to a directory. - ''; - }; - - executable = mkOption { - type = types.bool; - default = false; - description = "Whether the file should be executable."; - }; - }; - - config = { - target = mkDefault name; - source = mkIf (config.text != null) ( - let - file = pkgs.writeTextFile { - inherit (config) text executable; - name = "user-etc-" + baseNameOf name; - }; - in - mkDefault file - ); - }; - } - ))); + fileType = (import ../lib/file-type.nix { + inherit (config.home) homeDirectory; + inherit lib pkgs; + }).fileType; defaultCacheHome = "${config.home.homeDirectory}/.cache"; defaultConfigHome = "${config.home.homeDirectory}/.config"; @@ -81,7 +36,7 @@ in }; configFile = mkOption { - type = fileType "xdg.configHome" cfg.configHome; + type = fileType "xdg.configHome" cfg.configHome; default = {}; description = '' Attribute set of files to link into the user's XDG @@ -126,13 +81,7 @@ in }) { - home.file = - let - f = n: v: { - inherit (v) source target; - mode = if v.executable then "777" else "444"; - }; - in mapAttrsToList f cfg.configFile; + home.file = cfg.configFile; } ]; } diff --git a/modules/xsession.nix b/modules/xsession.nix index d51bb6b48..9f06e76be 100644 --- a/modules/xsession.nix +++ b/modules/xsession.nix @@ -105,7 +105,7 @@ in ''; home.file.".xsession" = { - mode = "555"; + executable = true; text = '' if [[ ! -v HM_XPROFILE_SOURCED ]]; then . ~/.xprofile