diff --git a/pkgs/build-support/writers/scripts.nix b/pkgs/build-support/writers/scripts.nix index 8a23e5dd4a66..edc7ede4d5bd 100644 --- a/pkgs/build-support/writers/scripts.nix +++ b/pkgs/build-support/writers/scripts.nix @@ -1,4 +1,14 @@ -{ pkgs, buildPackages, lib, stdenv, libiconv, mkNugetDeps, mkNugetSource, gixy }: +{ + buildPackages, + gixy, + lib, + libiconv, + makeBinaryWrapper, + mkNugetDeps, + mkNugetSource, + pkgs, + stdenv, +}: let inherit (lib) concatMapStringsSep @@ -6,7 +16,6 @@ let escapeShellArg last optionalString - stringLength strings types ; @@ -18,137 +27,285 @@ rec { # Examples: # writeBash = makeScriptWriter { interpreter = "${pkgs.bash}/bin/bash"; } # makeScriptWriter { interpreter = "${pkgs.dash}/bin/dash"; } "hello" "echo hello world" - makeScriptWriter = { interpreter, check ? "" }: nameOrPath: content: + makeScriptWriter = { interpreter, check ? "", makeWrapperArgs ? [], }: nameOrPath: content: assert lib.or (types.path.check nameOrPath) (builtins.match "([0-9A-Za-z._])[0-9A-Za-z._-]*" nameOrPath != null); assert lib.or (types.path.check content) (types.str.check content); let + nameIsPath = types.path.check nameOrPath; name = last (builtins.split "/" nameOrPath); - in + path = if nameIsPath then nameOrPath else "/bin/${name}"; + # The inner derivation which creates the executable under $out/bin (never at $out directly) + # This is required in order to support wrapping, as wrapped programs consist of at least two files: the executable and the wrapper. + inner = + pkgs.runCommandLocal name ( + { + inherit makeWrapperArgs; + nativeBuildInputs = [ + makeBinaryWrapper + ]; + meta.mainProgram = name; + } + // ( + if (types.str.check content) then { + inherit content interpreter; + passAsFile = [ "content" ]; + } else { + inherit interpreter; + contentPath = content; + } + ) + ) + '' + # On darwin a script cannot be used as an interpreter in a shebang but + # there doesn't seem to be a limit to the size of shebang and multiple + # arguments to the interpreter are allowed. + if [[ -n "${toString pkgs.stdenvNoCC.isDarwin}" ]] && isScript $interpreter + then + wrapperInterpreterLine=$(head -1 "$interpreter" | tail -c+3) + # Get first word from the line (note: xargs echo remove leading spaces) + wrapperInterpreter=$(echo "$wrapperInterpreterLine" | xargs echo | cut -d " " -f1) - pkgs.runCommandLocal name ( - lib.optionalAttrs (nameOrPath == "/bin/${name}") { - meta.mainProgram = name; - } - // ( - if (types.str.check content) then { - inherit content interpreter; - passAsFile = [ "content" ]; - } else { - inherit interpreter; - contentPath = content; - } - ) - ) - '' - # On darwin a script cannot be used as an interpreter in a shebang but - # there doesn't seem to be a limit to the size of shebang and multiple - # arguments to the interpreter are allowed. - if [[ -n "${toString pkgs.stdenvNoCC.isDarwin}" ]] && isScript $interpreter - then - wrapperInterpreterLine=$(head -1 "$interpreter" | tail -c+3) - # Get first word from the line (note: xargs echo remove leading spaces) - wrapperInterpreter=$(echo "$wrapperInterpreterLine" | xargs echo | cut -d " " -f1) + if isScript $wrapperInterpreter + then + echo "error: passed interpreter ($interpreter) is a script which has another script ($wrapperInterpreter) as an interpreter, which is not supported." + exit 1 + fi - if isScript $wrapperInterpreter - then - echo "error: passed interpreter ($interpreter) is a script which has another script ($wrapperInterpreter) as an interpreter, which is not supported." - exit 1 - fi + # This should work as long as wrapperInterpreter is a shell, which is + # the case for programs wrapped with makeWrapper, like + # python3.withPackages etc. + interpreterLine="$wrapperInterpreterLine $interpreter" + else + interpreterLine=$interpreter + fi - # This should work as long as wrapperInterpreter is a shell, which is - # the case for programs wrapped with makeWrapper, like - # python3.withPackages etc. - interpreterLine="$wrapperInterpreterLine $interpreter" - else - interpreterLine=$interpreter - fi + echo "#! $interpreterLine" > $out + cat "$contentPath" >> $out + ${optionalString (check != "") '' + ${check} $out + ''} + chmod +x $out + + # Relocate executable + # Wrap it if makeWrapperArgs are specified + mv $out tmp + mkdir -p $out/$(dirname "${path}") + mv tmp $out/${path} + if [ -n "''${makeWrapperArgs+''${makeWrapperArgs[@]}}" ]; then + wrapProgram $out/${path} ''${makeWrapperArgs[@]} + fi + ''; + in + if nameIsPath + then inner + # In case nameOrPath is a name, the user intends the executable to be located at $out. + # This is achieved by creating a separate derivation containing a symlink at $out linking to ${inner}/bin/${name}. + # This breaks the override pattern. + # In case this turns out to be a problem, we can still add more magic + else pkgs.runCommandLocal name {} '' + ln -s ${inner}/bin/${name} $out + ''; - echo "#! $interpreterLine" > $out - cat "$contentPath" >> $out - ${optionalString (check != "") '' - ${check} $out - ''} - chmod +x $out - ${optionalString (types.path.check nameOrPath) '' - mv $out tmp - mkdir -p $out/$(dirname "${nameOrPath}") - mv tmp $out/${nameOrPath} - ''} - ''; # Base implementation for compiled executables. # Takes a compile script, which in turn takes the name as an argument. # # Examples: # writeSimpleC = makeBinWriter { compileScript = name: "gcc -o $out $contentPath"; } - makeBinWriter = { compileScript, strip ? true }: nameOrPath: content: + makeBinWriter = { compileScript, strip ? true, makeWrapperArgs ? [] }: nameOrPath: content: assert lib.or (types.path.check nameOrPath) (builtins.match "([0-9A-Za-z._])[0-9A-Za-z._-]*" nameOrPath != null); assert lib.or (types.path.check content) (types.str.check content); let + nameIsPath = types.path.check nameOrPath; name = last (builtins.split "/" nameOrPath); + path = if nameIsPath then nameOrPath else "/bin/${name}"; + # The inner derivation which creates the executable under $out/bin (never at $out directly) + # This is required in order to support wrapping, as wrapped programs consist of at least two files: the executable and the wrapper. + inner = + pkgs.runCommandLocal name ( + { + inherit makeWrapperArgs; + nativeBuildInputs = [ + makeBinaryWrapper + ]; + meta.mainProgram = name; + } + // ( + if (types.str.check content) then { + inherit content; + passAsFile = [ "content" ]; + } else { + contentPath = content; + } + ) + ) + '' + ${compileScript} + ${lib.optionalString strip + "${lib.getBin buildPackages.bintools-unwrapped}/bin/${buildPackages.bintools-unwrapped.targetPrefix}strip -S $out"} + # Sometimes binaries produced for darwin (e. g. by GHC) won't be valid + # mach-o executables from the get-go, but need to be corrected somehow + # which is done by fixupPhase. + ${lib.optionalString pkgs.stdenvNoCC.hostPlatform.isDarwin "fixupPhase"} + mv $out tmp + mkdir -p $out/$(dirname "${path}") + mv tmp $out/${path} + if [ -n "''${makeWrapperArgs+''${makeWrapperArgs[@]}}" ]; then + wrapProgram $out/${path} ''${makeWrapperArgs[@]} + fi + ''; in - pkgs.runCommand name ((if (types.str.check content) then { - inherit content; - passAsFile = [ "content" ]; - } else { - contentPath = content; - }) // lib.optionalAttrs (nameOrPath == "/bin/${name}") { - meta.mainProgram = name; - }) '' - ${compileScript} - ${lib.optionalString strip - "${lib.getBin buildPackages.bintools-unwrapped}/bin/${buildPackages.bintools-unwrapped.targetPrefix}strip -S $out"} - # Sometimes binaries produced for darwin (e. g. by GHC) won't be valid - # mach-o executables from the get-go, but need to be corrected somehow - # which is done by fixupPhase. - ${lib.optionalString pkgs.stdenvNoCC.hostPlatform.isDarwin "fixupPhase"} - ${optionalString (types.path.check nameOrPath) '' - mv $out tmp - mkdir -p $out/$(dirname "${nameOrPath}") - mv tmp $out/${nameOrPath} - ''} - ''; + if nameIsPath + then inner + # In case nameOrPath is a name, the user intends the executable to be located at $out. + # This is achieved by creating a separate derivation containing a symlink at $out linking to ${inner}/bin/${name}. + # This breaks the override pattern. + # In case this turns out to be a problem, we can still add more magic + else pkgs.runCommandLocal name {} '' + ln -s ${inner}/bin/${name} $out + ''; # Like writeScript but the first line is a shebang to bash # - # Example: + # Can be called with or without extra arguments. + # + # Example without arguments: # writeBash "example" '' # echo hello world # '' - writeBash = makeScriptWriter { - interpreter = "${lib.getExe pkgs.bash}"; - }; + # + # Example with arguments: + # writeBash "example" + # { + # makeWrapperArgs = [ + # "--prefix" "PATH" ":" "${pkgs.hello}/bin" + # ]; + # } + # '' + # hello + # '' + writeBash = name: argsOrScript: + if lib.isAttrs argsOrScript && ! lib.isDerivation argsOrScript + then makeScriptWriter (argsOrScript // { interpreter = "${lib.getExe pkgs.bash}"; }) name + else makeScriptWriter { interpreter = "${lib.getExe pkgs.bash}"; } name argsOrScript; # Like writeScriptBin but the first line is a shebang to bash + # + # Can be called with or without extra arguments. + # + # Example without arguments: + # writeBashBin "example" '' + # echo hello world + # '' + # + # Example with arguments: + # writeBashBin "example" + # { + # makeWrapperArgs = [ + # "--prefix", "PATH", ":", "${pkgs.hello}/bin", + # ]; + # } + # '' + # hello + # '' writeBashBin = name: writeBash "/bin/${name}"; # Like writeScript but the first line is a shebang to dash # - # Example: + # Can be called with or without extra arguments. + # + # Example without arguments: # writeDash "example" '' # echo hello world # '' - writeDash = makeScriptWriter { - interpreter = "${lib.getExe pkgs.dash}"; - }; + # + # Example with arguments: + # writeDash "example" + # { + # makeWrapperArgs = [ + # "--prefix", "PATH", ":", "${pkgs.hello}/bin", + # ]; + # } + # '' + # hello + # '' + writeDash = name: argsOrScript: + if lib.isAttrs argsOrScript && ! lib.isDerivation argsOrScript + then makeScriptWriter (argsOrScript // { interpreter = "${lib.getExe pkgs.dash}"; }) name + else makeScriptWriter { interpreter = "${lib.getExe pkgs.dash}"; } name argsOrScript; # Like writeScriptBin but the first line is a shebang to dash + # + # Can be called with or without extra arguments. + # + # Example without arguments: + # writeDashBin "example" '' + # echo hello world + # '' + # + # Example with arguments: + # writeDashBin "example" + # { + # makeWrapperArgs = [ + # "--prefix", "PATH", ":", "${pkgs.hello}/bin", + # ]; + # } + # '' + # hello + # '' writeDashBin = name: writeDash "/bin/${name}"; # Like writeScript but the first line is a shebang to fish # - # Example: + # Can be called with or without extra arguments. + # + # Example without arguments: # writeFish "example" '' # echo hello world # '' - writeFish = makeScriptWriter { - interpreter = "${lib.getExe pkgs.fish} --no-config"; - check = "${lib.getExe pkgs.fish} --no-config --no-execute"; # syntax check only - }; + # + # Example with arguments: + # writeFish "example" + # { + # makeWrapperArgs = [ + # "--prefix", "PATH", ":", "${pkgs.hello}/bin", + # ]; + # } + # '' + # hello + # '' + writeFish = name: argsOrScript: + if lib.isAttrs argsOrScript && ! lib.isDerivation argsOrScript + then makeScriptWriter (argsOrScript // { + interpreter = "${lib.getExe pkgs.fish} --no-config"; + check = "${lib.getExe pkgs.fish} --no-config --no-execute"; # syntax check only + }) name + else makeScriptWriter { + interpreter = "${lib.getExe pkgs.fish} --no-config"; + check = "${lib.getExe pkgs.fish} --no-config --no-execute"; # syntax check only + } name argsOrScript; # Like writeScriptBin but the first line is a shebang to fish + # + # Can be called with or without extra arguments. + # + # Example without arguments: + # writeFishBin "example" '' + # echo hello world + # '' + # + # Example with arguments: + # writeFishBin "example" + # { + # makeWrapperArgs = [ + # "--prefix", "PATH", ":", "${pkgs.hello}/bin", + # ]; + # } + # '' + # hello + # '' writeFishBin = name: writeFish "/bin/${name}"; @@ -162,11 +319,12 @@ rec { # main = launchMissiles # ''; writeHaskell = name: { - libraries ? [], ghc ? pkgs.ghc, ghcArgs ? [], + libraries ? [], + makeWrapperArgs ? [], + strip ? true, threadedRuntime ? true, - strip ? true }: let appendIfNotSet = el: list: if elem el list then list else list ++ [ el ]; @@ -178,7 +336,7 @@ rec { ${(ghc.withPackages (_: libraries ))}/bin/ghc ${lib.escapeShellArgs ghcArgs'} tmp.hs mv tmp $out ''; - inherit strip; + inherit makeWrapperArgs strip; } name; # writeHaskellBin takes the same arguments as writeHaskell but outputs a directory (like writeScriptBin) @@ -187,36 +345,72 @@ rec { # Like writeScript but the first line is a shebang to nu # - # Example: + # Can be called with or without extra arguments. + # + # Example without arguments: # writeNu "example" '' # echo hello world # '' - writeNu = makeScriptWriter { - interpreter = "${lib.getExe pkgs.nushell} --no-config-file"; - }; + # + # Example with arguments: + # writeNu "example" + # { + # makeWrapperArgs = [ + # "--prefix", "PATH", ":", "${pkgs.hello}/bin", + # ]; + # } + # '' + # hello + # '' + writeNu = name: argsOrScript: + if lib.isAttrs argsOrScript && ! lib.isDerivation argsOrScript + then makeScriptWriter (argsOrScript // { interpreter = "${lib.getExe pkgs.nushell} --no-config-file"; }) name + else makeScriptWriter { interpreter = "${lib.getExe pkgs.nushell} --no-config-file"; } name argsOrScript; + # Like writeScriptBin but the first line is a shebang to nu + # + # Can be called with or without extra arguments. + # + # Example without arguments: + # writeNuBin "example" '' + # echo hello world + # '' + # + # Example with arguments: + # writeNuBin "example" + # { + # makeWrapperArgs = [ + # "--prefix", "PATH", ":", "${pkgs.hello}/bin", + # ]; + # } + # '' + # hello + # '' writeNuBin = name: writeNu "/bin/${name}"; # makeRubyWriter takes ruby and compatible rubyPackages and produces ruby script writer, # If any libraries are specified, ruby.withPackages is used as interpreter, otherwise the "bare" ruby is used. - makeRubyWriter = ruby: rubyPackages: buildRubyPackages: name: { libraries ? [], }: - makeScriptWriter { - interpreter = - if libraries == [] - then "${ruby}/bin/ruby" - else "${(ruby.withPackages (ps: libraries))}/bin/ruby"; - # Rubocop doesnt seem to like running in this fashion. - #check = (writeDash "rubocop.sh" '' - # exec ${lib.getExe buildRubyPackages.rubocop} "$1" - #''); - } name; + makeRubyWriter = ruby: rubyPackages: buildRubyPackages: name: { libraries ? [], ... } @ args: + makeScriptWriter ( + (builtins.removeAttrs args ["libraries"]) + // { + interpreter = + if libraries == [] + then "${ruby}/bin/ruby" + else "${(ruby.withPackages (ps: libraries))}/bin/ruby"; + # Rubocop doesn't seem to like running in this fashion. + #check = (writeDash "rubocop.sh" '' + # exec ${lib.getExe buildRubyPackages.rubocop} "$1" + #''); + } + ) name; # Like writeScript but the first line is a shebang to ruby # # Example: - # writeRuby "example" '' + # writeRuby "example" { libraries = [ pkgs.rubyPackages.git ]; } '' # puts "hello world" # '' writeRuby = makeRubyWriter pkgs.ruby pkgs.rubyPackages buildPackages.rubyPackages; @@ -227,17 +421,20 @@ rec { # makeLuaWriter takes lua and compatible luaPackages and produces lua script writer, # which validates the script with luacheck at build time. If any libraries are specified, # lua.withPackages is used as interpreter, otherwise the "bare" lua is used. - makeLuaWriter = lua: luaPackages: buildLuaPackages: name: { libraries ? [], }: - makeScriptWriter { - interpreter = lua.interpreter; - # if libraries == [] - # then lua.interpreter - # else (lua.withPackages (ps: libraries)).interpreter - # This should support packages! I just cant figure out why some dependency collision happens whenever I try to run this. - check = (writeDash "luacheck.sh" '' - exec ${buildLuaPackages.luacheck}/bin/luacheck "$1" - ''); - } name; + makeLuaWriter = lua: luaPackages: buildLuaPackages: name: { libraries ? [], ... } @ args: + makeScriptWriter ( + (builtins.removeAttrs args ["libraries"]) + // { + interpreter = lua.interpreter; + # if libraries == [] + # then lua.interpreter + # else (lua.withPackages (ps: libraries)).interpreter + # This should support packages! I just cant figure out why some dependency collision happens whenever I try to run this. + check = (writeDash "luacheck.sh" '' + exec ${buildLuaPackages.luacheck}/bin/luacheck "$1" + ''); + } + ) name; # writeLua takes a name an attributeset with libraries and some lua source code and # returns an executable (should also work with luajit) @@ -265,9 +462,10 @@ rec { writeLua "/bin/${name}"; writeRust = name: { - rustc ? pkgs.rustc, - rustcArgs ? [], - strip ? true + makeWrapperArgs ? [], + rustc ? pkgs.rustc, + rustcArgs ? [], + strip ? true, }: let darwinArgs = lib.optionals stdenv.isDarwin [ "-L${lib.getLib libiconv}/lib" ]; @@ -277,7 +475,7 @@ rec { cp "$contentPath" tmp.rs PATH=${lib.makeBinPath [pkgs.gcc]} ${rustc}/bin/rustc ${lib.escapeShellArgs rustcArgs} ${lib.escapeShellArgs darwinArgs} -o "$out" tmp.rs ''; - inherit strip; + inherit makeWrapperArgs strip; } name; writeRustBin = name: @@ -337,10 +535,13 @@ rec { # use boolean; # print "Howdy!\n" if true; # '' - writePerl = name: { libraries ? [] }: - makeScriptWriter { - interpreter = "${lib.getExe (pkgs.perl.withPackages (p: libraries))}"; - } name; + writePerl = name: { libraries ? [], ... } @ args: + makeScriptWriter ( + (builtins.removeAttrs args ["libraries"]) + // { + interpreter = "${lib.getExe (pkgs.perl.withPackages (p: libraries))}"; + } + ) name; # writePerlBin takes the same arguments as writePerl but outputs a directory (like writeScriptBin) writePerlBin = name: @@ -349,22 +550,27 @@ rec { # makePythonWriter takes python and compatible pythonPackages and produces python script writer, # which validates the script with flake8 at build time. If any libraries are specified, # python.withPackages is used as interpreter, otherwise the "bare" python is used. - makePythonWriter = python: pythonPackages: buildPythonPackages: name: { libraries ? [], flakeIgnore ? [] }: + makePythonWriter = python: pythonPackages: buildPythonPackages: name: { libraries ? [], flakeIgnore ? [], ... } @ args: let ignoreAttribute = optionalString (flakeIgnore != []) "--ignore ${concatMapStringsSep "," escapeShellArg flakeIgnore}"; in - makeScriptWriter { - interpreter = - if pythonPackages != pkgs.pypy2Packages || pythonPackages != pkgs.pypy3Packages then - if libraries == [] - then python.interpreter - else (python.withPackages (ps: libraries)).interpreter - else python.interpreter - ; - check = optionalString python.isPy3k (writeDash "pythoncheck.sh" '' - exec ${buildPythonPackages.flake8}/bin/flake8 --show-source ${ignoreAttribute} "$1" - ''); - } name; + makeScriptWriter + ( + (builtins.removeAttrs args ["libraries" "flakeIgnore"]) + // { + interpreter = + if pythonPackages != pkgs.pypy2Packages || pythonPackages != pkgs.pypy3Packages then + if libraries == [] + then python.interpreter + else (python.withPackages (ps: libraries)).interpreter + else python.interpreter + ; + check = optionalString python.isPy3k (writeDash "pythoncheck.sh" '' + exec ${buildPythonPackages.flake8}/bin/flake8 --show-source ${ignoreAttribute} "$1" + ''); + } + ) + name; # writePyPy2 takes a name an attributeset with libraries and some pypy2 sourcecode and # returns an executable @@ -421,7 +627,7 @@ rec { writePyPy3 "/bin/${name}"; - makeFSharpWriter = { dotnet-sdk ? pkgs.dotnet-sdk, fsi-flags ? "", libraries ? _: [] }: nameOrPath: + makeFSharpWriter = { dotnet-sdk ? pkgs.dotnet-sdk, fsi-flags ? "", libraries ? _: [], ... } @ args: nameOrPath: let fname = last (builtins.split "/" nameOrPath); path = if strings.hasSuffix ".fsx" nameOrPath then nameOrPath else "${nameOrPath}.fsx"; @@ -442,9 +648,12 @@ rec { ${lib.getExe dotnet-sdk} fsi --quiet --nologo --readline- ${fsi-flags} "$@" < "$script" ''; - in content: makeScriptWriter { - interpreter = fsi; - } path + in content: makeScriptWriter ( + (builtins.removeAttrs args ["dotnet-sdk" "fsi-flags" "libraries"]) + // { + interpreter = fsi; + } + ) path '' #i "nuget: ${nuget-source}/lib" ${ content } @@ -456,5 +665,4 @@ rec { writeFSharpBin = name: writeFSharp "/bin/${name}"; - } diff --git a/pkgs/build-support/writers/test.nix b/pkgs/build-support/writers/test.nix index 982c550d28e0..df0eb340d9ae 100644 --- a/pkgs/build-support/writers/test.nix +++ b/pkgs/build-support/writers/test.nix @@ -1,13 +1,8 @@ -{ glib -, haskellPackages +{ haskellPackages , lib , nodePackages , perlPackages -, pypy2Packages , python3Packages -, pypy3Packages -, luaPackages -, rubyPackages , runCommand , testers , writers @@ -310,4 +305,85 @@ lib.recurseIntoAttrs { expected = "hello: world\n"; }; }; + + wrapping = lib.recurseIntoAttrs { + bash-bin = expectSuccessBin ( + writeBashBin "test-writers-wrapping-bash-bin" + { + makeWrapperArgs = [ + "--set" + "ThaigerSprint" + "Thailand" + ]; + } + '' + if [[ "$ThaigerSprint" == "Thailand" ]]; then + echo "success" + fi + '' + ); + + bash = expectSuccess ( + writeBash "test-writers-wrapping-bash" + { + makeWrapperArgs = [ + "--set" + "ThaigerSprint" + "Thailand" + ]; + } + '' + if [[ "$ThaigerSprint" == "Thailand" ]]; then + echo "success" + fi + '' + ); + + python = expectSuccess ( + writePython3 "test-writers-wrapping-python" + { + makeWrapperArgs = [ + "--set" + "ThaigerSprint" + "Thailand" + ]; + } + '' + import os + + if os.environ.get("ThaigerSprint") == "Thailand": + print("success") + '' + ); + + rust = expectSuccess ( + writeRust "test-writers-wrapping-rust" + { + makeWrapperArgs = [ + "--set" + "ThaigerSprint" + "Thailand" + ]; + } + '' + fn main(){ + if std::env::var("ThaigerSprint").unwrap() == "Thailand" { + println!("success") + } + } + '' + ); + + no-empty-wrapper = let + bin = writeBashBin "bin" { makeWrapperArgs = []; } ''true''; + in runCommand "run-test-writers-wrapping-no-empty-wrapper" {} '' + ls -A ${bin}/bin + if [ $(ls -A ${bin}/bin | wc -l) -eq 1 ]; then + touch $out + else + echo "Error: Empty wrapper was created" >&2 + exit 1 + fi + ''; + }; }