├── .gitignore ├── AppRun.c ├── LICENSE ├── Makefile ├── README.md ├── appdir.nix ├── appdir.sh ├── appimage-bundle.nix ├── appimage-top.nix ├── appimage.nix ├── appimagetool.nix ├── default.nix ├── flake.lock ├── flake.nix ├── install-nix-from-closure.sh ├── nix-bootstrap.sh ├── nix-bundle.sh ├── nix-installer.nix ├── nix-run.sh ├── nix-strace.sh ├── nix-user-chroot ├── Makefile └── main.cpp ├── nix2appimage.sh ├── proot-x86_64 ├── release.nix ├── test-appimage.nix └── test.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | result* -------------------------------------------------------------------------------- /AppRun.c: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | 3 | Copyright (c) 2004-16 Simon Peter 4 | Portions Copyright (c) 2010 RazZziel 5 | 6 | All Rights Reserved. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | 26 | **************************************************************************/ 27 | 28 | #define _GNU_SOURCE 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | 41 | #define die(...) \ 42 | do { \ 43 | fprintf(stderr, "Error: " __VA_ARGS__); \ 44 | exit(1); \ 45 | } while (0); 46 | 47 | #define PATH_MAX 4096 48 | 49 | #define LINE_SIZE 255 50 | 51 | #define err_exit(format, ...) { fprintf(stderr, format ": %s\n", ##__VA_ARGS__, strerror(errno)); exit(EXIT_FAILURE); } 52 | 53 | int filter (const struct dirent *dir) { 54 | char *p = (char*) &dir->d_name; 55 | p = strrchr(p, '.'); 56 | return p && !strcmp(p, ".desktop"); 57 | } 58 | 59 | static void update_map(char *mapping, char *map_file) { 60 | int fd; 61 | 62 | fd = open(map_file, O_WRONLY); 63 | if (fd < 0) { 64 | err_exit("map open"); 65 | } 66 | 67 | int map_len = strlen(mapping); 68 | if (write(fd, mapping, map_len) != map_len) { 69 | err_exit("map write"); 70 | } 71 | 72 | close(fd); 73 | } 74 | 75 | static void add_path(const char* name, const char* rootdir) { 76 | char path_buf[PATH_MAX]; 77 | snprintf(path_buf, sizeof(path_buf), "/%s", name); 78 | 79 | struct stat statbuf; 80 | if (stat(path_buf, &statbuf) < 0) { 81 | fprintf(stderr, "Cannot stat %s: %s\n", path_buf, strerror(errno)); 82 | return; 83 | } 84 | 85 | char path_buf2[PATH_MAX]; 86 | snprintf(path_buf2, sizeof(path_buf2), "%s/%s", rootdir, name); 87 | 88 | mkdir(path_buf2, statbuf.st_mode & ~S_IFMT); 89 | if (mount(path_buf, path_buf2, "none", MS_BIND | MS_REC, NULL) < 0) { 90 | fprintf(stderr, "Cannot bind mount %s to %s: %s\n", path_buf, path_buf2, strerror(errno)); 91 | } 92 | } 93 | 94 | #define SAVE_ENV_VAR(x) char *x = getenv(#x) 95 | #define LOAD_ENV_VAR(x) do { if (x != NULL) setenv(#x, x, 1); } while(0) 96 | 97 | int main(int argc, char *argv[]) { 98 | char *appdir = dirname(realpath("/proc/self/exe", NULL)); 99 | if (!appdir) 100 | die("Could not access /proc/self/exe\n"); 101 | 102 | char *tmpdir = getenv("TMPDIR"); 103 | if (!tmpdir) { 104 | tmpdir = "/tmp"; 105 | } 106 | 107 | char template[PATH_MAX]; 108 | int needed = snprintf(template, PATH_MAX, "%s/nixXXXXXX", tmpdir); 109 | if (needed < 0) { 110 | err_exit("TMPDIR too long: '%s'", tmpdir); 111 | } 112 | 113 | char *rootdir = mkdtemp(template); 114 | if (!rootdir) { 115 | err_exit("mkdtemp(%s)", template); 116 | } 117 | 118 | int ret; 119 | 120 | struct dirent **namelist; 121 | 122 | ret = scandir(appdir, &namelist, filter, NULL); 123 | 124 | if (ret == 0) { 125 | die("No .desktop files found\n"); 126 | } else if(ret == -1) { 127 | die("Could not scan directory %s\n", appdir); 128 | } 129 | 130 | /* Extract executable from .desktop file */ 131 | 132 | FILE *f; 133 | char *desktop_file = malloc(LINE_SIZE); 134 | snprintf(desktop_file, LINE_SIZE-1, "%s/%s", appdir, namelist[0]->d_name); 135 | f = fopen(desktop_file, "r"); 136 | 137 | char *line = malloc(LINE_SIZE); 138 | size_t n = LINE_SIZE; 139 | int found = 0; 140 | 141 | while (getline(&line, &n, f) != -1) 142 | { 143 | if (!strncmp(line,"Exec=",5)) 144 | { 145 | char *p = line+5; 146 | while (*++p && *p != ' ' && *p != '%' && *p != '\n'); 147 | *p = 0; 148 | found = 1; 149 | break; 150 | } 151 | } 152 | 153 | fclose(f); 154 | 155 | if (!found) 156 | die("Executable not found, make sure there is a line starting with 'Exec='\n"); 157 | 158 | /* Execution */ 159 | char *executable = basename(line+5); 160 | 161 | char full_exec[PATH_MAX]; 162 | snprintf(full_exec, sizeof(full_exec), "/usr/bin/%s", executable); 163 | 164 | // get uid, gid before going to new namespace 165 | uid_t uid = getuid(); 166 | gid_t gid = getgid(); 167 | 168 | // "unshare" into new namespace 169 | if (unshare(CLONE_NEWNS | CLONE_NEWUSER) < 0) { 170 | err_exit("unshare()"); 171 | } 172 | 173 | // add necessary system stuff to rootdir namespace 174 | add_path("dev", rootdir); 175 | add_path("proc", rootdir); 176 | add_path("sys", rootdir); 177 | add_path("run", rootdir); 178 | add_path("etc", rootdir); 179 | add_path("home", rootdir); 180 | 181 | // setup skeleton 182 | char path_buf[PATH_MAX]; 183 | snprintf(path_buf, sizeof(path_buf), "%s/tmp", rootdir); 184 | mkdir(path_buf, ~0); 185 | snprintf(path_buf, sizeof(path_buf), "%s/var", rootdir); 186 | mkdir(path_buf, ~0); 187 | 188 | // make sure nixdir exists 189 | struct stat statbuf2; 190 | if (stat(appdir, &statbuf2) < 0) { 191 | err_exit("stat(%s)", appdir); 192 | } 193 | 194 | char nixdir[PATH_MAX]; 195 | snprintf(nixdir, sizeof(nixdir), "%s/nix", appdir); 196 | snprintf(path_buf, sizeof(path_buf), "%s/nix", rootdir); 197 | mkdir(path_buf, statbuf2.st_mode & ~S_IFMT); 198 | if (mount(nixdir, path_buf, "none", MS_BIND | MS_REC, NULL) < 0) { 199 | err_exit("mount(%s, %s)", nixdir, path_buf); 200 | } 201 | 202 | char usrdir[PATH_MAX]; 203 | snprintf(usrdir, sizeof(usrdir), "%s/usr", appdir); 204 | snprintf(path_buf, sizeof(path_buf), "%s/usr", rootdir); 205 | mkdir(path_buf, statbuf2.st_mode & ~S_IFMT); 206 | if (mount(usrdir, path_buf, "none", MS_BIND | MS_REC, NULL) < 0) { 207 | err_exit("mount(%s, %s)", usrdir, path_buf); 208 | } 209 | 210 | snprintf(path_buf, sizeof(path_buf), "%s/bin", rootdir); 211 | if (symlink("/usr/bin", path_buf) < 0) { 212 | err_exit("symlink(/usr/bin, %s)", path_buf); 213 | } 214 | 215 | // fixes issue #1 where writing to /proc/self/gid_map fails 216 | // see user_namespaces(7) for more documentation 217 | int fd_setgroups = open("/proc/self/setgroups", O_WRONLY); 218 | if (fd_setgroups > 0) { 219 | write(fd_setgroups, "deny", 4); 220 | } 221 | 222 | // map the original uid/gid in the new ns 223 | char map_buf[1024]; 224 | snprintf(map_buf, sizeof(map_buf), "%d %d 1", uid, uid); 225 | update_map(map_buf, "/proc/self/uid_map"); 226 | snprintf(map_buf, sizeof(map_buf), "%d %d 1", gid, gid); 227 | update_map(map_buf, "/proc/self/gid_map"); 228 | 229 | // chroot to rootdir 230 | if (chroot(rootdir) < 0) { 231 | err_exit("chroot(%s)", rootdir); 232 | } 233 | 234 | char *pwddir = getenv("PWD"); 235 | chdir(pwddir); 236 | 237 | SAVE_ENV_VAR(PWD); 238 | SAVE_ENV_VAR(DBUS_SESSION_BUS_ADDRESS); 239 | SAVE_ENV_VAR(USER); 240 | SAVE_ENV_VAR(HOSTNAME); 241 | SAVE_ENV_VAR(LANG); 242 | SAVE_ENV_VAR(LC_ALL); 243 | SAVE_ENV_VAR(TERM); 244 | SAVE_ENV_VAR(DISPLAY); 245 | SAVE_ENV_VAR(XDG_RUNTIME_DIR); 246 | SAVE_ENV_VAR(XAUTHORITY); 247 | SAVE_ENV_VAR(XDG_SESSION_ID); 248 | SAVE_ENV_VAR(XDG_SEAT); 249 | SAVE_ENV_VAR(HOME); 250 | 251 | clearenv(); 252 | 253 | LOAD_ENV_VAR(PWD); 254 | LOAD_ENV_VAR(DBUS_SESSION_BUS_ADDRESS); 255 | LOAD_ENV_VAR(USER); 256 | LOAD_ENV_VAR(HOSTNAME); 257 | LOAD_ENV_VAR(LANG); 258 | LOAD_ENV_VAR(LC_ALL); 259 | LOAD_ENV_VAR(TERM); 260 | LOAD_ENV_VAR(DISPLAY); 261 | LOAD_ENV_VAR(XDG_RUNTIME_DIR); 262 | LOAD_ENV_VAR(XAUTHORITY); 263 | LOAD_ENV_VAR(XDG_SESSION_ID); 264 | LOAD_ENV_VAR(XDG_SEAT); 265 | LOAD_ENV_VAR(HOME); 266 | 267 | setenv("PATH", ENV_PATH, 1); 268 | setenv("TMPDIR", "/tmp", 1); 269 | 270 | /* Run */ 271 | // FIXME: What about arguments in the Exec= line of the desktop file? 272 | ret = execvp(full_exec, argv); 273 | 274 | if (ret == -1) 275 | die("Error executing '%s'; return code: %d\n", full_exec, ret); 276 | 277 | free(line); 278 | free(desktop_file); 279 | return 0; 280 | } 281 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Matthew Justin Bauer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX ?= /usr 2 | 3 | install: nix-bundle.sh nix-run.sh appdir.nix appimagetool.nix appimage.nix AppRun.c appimage-top.nix default.nix appdir.sh nix2appimage.sh nix-user-chroot/ 4 | mkdir -p ${PREFIX}/share/nix-bundle/ 5 | cp -r $^ ${PREFIX}/share/nix-bundle/ 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UNSTABLE, Breaking changes may be done without warning. 2 | 3 | # nix-bundle 4 | 5 | nix-bundle is a way to package Nix attributes into single-file executables. 6 | 7 | ## Benefits 8 | 9 | * Single-file output 10 | * Can be run by non-root users 11 | * No runtime 12 | * Distro agnostic 13 | * No installation 14 | 15 | ## Getting started 16 | 17 | Make sure you have installed Nix already. See http://nixos.org/nix/ for more details. 18 | 19 | Once you have a working Nix install, you can run: 20 | 21 | ```sh 22 | $ ./nix-bundle.sh hello /bin/hello 23 | ``` 24 | 25 | ```hello``` indicates the Nix derivation from NixPkgs that you want to use, while ```/bin/hello``` indicates the path of the executable relative to ```hello``` that you want to run. This will create the file "hello". Running it: 26 | 27 | ```sh 28 | $ ./hello 29 | Hello, world! 30 | ``` 31 | 32 | This is a standalone file that is completely portable! As long as you are running the same architecture Linux kernel and have a shell interpreter available it will run. 33 | 34 | Some others to try: 35 | 36 | ```sh 37 | ./nix-bundle.sh nano /bin/nano 38 | ``` 39 | 40 | ```sh 41 | ./nix-bundle.sh emacs /bin/emacs 42 | ``` 43 | 44 | Or if you want to try graphical applications: 45 | 46 | ```sh 47 | # Simple X game. Very few dependencies. Quick to build and load. ~13MB 48 | ./nix-bundle.sh xskat /bin/xskat 49 | ``` 50 | 51 | ```sh 52 | ./nix-bundle.sh firefox /bin/firefox 53 | ``` 54 | 55 | ```sh 56 | # SDL-based game. ~228MB 57 | ./nix-bundle.sh ivan /bin/ivan 58 | ``` 59 | 60 | 61 | ## Self-bundling (meta) 62 | 63 | Starting with v0.1.3, you can bundle nix-bundle! To do this, just use nix-bundle normally: 64 | 65 | ```sh 66 | NIX_PATH="nixpkgs=https://github.com/matthewbauer/nixpkgs/archive/nix-bundle.tar.gz" ./nix-bundle.sh nix-bundle /bin/nix-bundle 67 | ``` 68 | 69 | ## [Experimental] Create AppImage executables from Nix expressions 70 | 71 | "nix-bundle.sh" tends to create fairly large outputs. This is largely because nix-bundle.sh "extracts" its payload up front. AppImage uses a different method where extraction only takes place when the file is accessed (through FUSE and SquashFS). You can now create a compliant "AppImage" using the "nix2appimage.sh" script: 72 | 73 | ```sh 74 | ./nix2appimage.sh emacs 75 | ``` 76 | 77 | This will create a file at Emacs-x86_64.AppImage which you can execute. 78 | 79 | Notice that there is only one argument for nix2appimage.sh. This is because the target executable will be detected from the .desktop file in ```/share/applications/*.desktop```. As a side-effect, AppImage requires your package to have a .desktop file, so packages like "hello", "coreutils", etc. will not work. 80 | 81 | Some other examples to try: 82 | 83 | ```sh 84 | ./nix2appimage.sh firefox 85 | ``` 86 | 87 | ```sh 88 | ./nix2appimage.sh vlc 89 | ``` 90 | 91 | ```sh 92 | ./nix2appimage.sh 0ad 93 | ``` 94 | 95 | ```sh 96 | ./nix2appimage.sh wireshark-gtk 97 | ``` 98 | 99 | These may take a while because of the large closure size. 100 | 101 | Note that these do not currently work out of the box with NixOS. Other Linux distros should work. 102 | 103 | ## Comparison with AppImage, FlatPak, Snap 104 | 105 | | Name | Distro-agnostic | Runtime required | Root required | Storage | 106 | | ---------- | --------------- | ---------------- | ------------- | ------- | 107 | | nix-bundle | yes | no | no | Arx tarball | 108 | | AppImage | yes | no | no | Squashfs w/ lzma compression | 109 | | FlatPak | yes | yes | no | ? | 110 | | Snap | yes | yes | no | squashFS | 111 | 112 | ## How it works 113 | 114 | Nix-bundle glues together four different projects to work correctly: 115 | 116 | * [Arx](https://github.com/solidsnack/arx) - an archive execution tool 117 | * Creates single-file archive executable that can unpack themselves and then run some command. nix-bundle calls nix-user-chroot to bootstrap the Nix environment. It outputs a "./nix" folder. 118 | * [nix-user-chroot](https://github.com/lethalman/nix-user-chroot) - a small bootstrap that uses Linux namespaces to call chroot 119 | * This will create sub namespace and bind mount the "./nix" to "/nix" so that the Nix references function properly. 120 | * [Nix](https://nixos.org/nix/) - a functional package manager 121 | * Used to build runtime closures that are self-contained. 122 | * [nixpkgs](https://nixos.org/nixpkgs/) 123 | * Provides lots of different packages to choose from. 124 | 125 | ## Drawbacks 126 | 127 | Nix-bundle has some drawbacks that need to be worked on: 128 | 129 | * Slow startup 130 | * Large files (Firefox 150MB) 131 | * Only compatible Linux 132 | * Outputs built on x86-64 will not run on i386 133 | * Requires Linux kernel with CAP_SYS_USER_NS on and permissions setup correctly 134 | -------------------------------------------------------------------------------- /appdir.nix: -------------------------------------------------------------------------------- 1 | { stdenv, lib, fetchurl, muslPkgs, perl, pathsFromGraph, fetchFromGitHub, coreutils, bash }: 2 | 3 | let 4 | AppRun = targets: muslPkgs.stdenv.mkDerivation { 5 | name = "AppRun"; 6 | 7 | phases = [ "buildPhase" "installPhase" "fixupPhase" ]; 8 | 9 | buildPhase = '' 10 | CC="$CC -O2 -Wall -Wno-deprecated-declarations -Wno-unused-result -static" 11 | $CC ${./AppRun.c} -o AppRun -DENV_PATH='"${lib.makeBinPath targets}"' 12 | ''; 13 | 14 | installPhase = '' 15 | mkdir -p $out/bin 16 | cp AppRun $out/bin/AppRun 17 | ''; 18 | }; 19 | 20 | in 21 | 22 | { target, name, extraTargets ? [ coreutils bash ] }: let targets = ([ target ] ++ extraTargets); 23 | in stdenv.mkDerivation { 24 | name = "${name}.AppDir"; 25 | exportReferencesGraph = map (x: [("closure-" + baseNameOf x) x]) targets; 26 | nativeBuildInputs = [ perl ]; 27 | buildCommand = '' 28 | # TODO use symlinks to shrink output size 29 | 30 | if [ ! -d ${target}/share/applications ]; then 31 | echo "--------------------------------------------------" 32 | echo "| /share/applications does not exist. |" 33 | echo "| AppImage only works with 'applications'. |" 34 | echo "| Try using nix-bundle.sh for command-line apps. |" 35 | echo "--------------------------------------------------" 36 | exit 1 37 | fi 38 | 39 | storePaths=$(${perl}/bin/perl ${pathsFromGraph} ./closure-*) 40 | 41 | mkdir -p $out/${name}.AppDir 42 | cd $out/${name}.AppDir 43 | 44 | mkdir -p nix/store 45 | cp -r $storePaths nix/store 46 | 47 | ln -s .${target} usr 48 | 49 | if [ -d ${target}/share/appdata ]; then 50 | chmod a+w usr/share 51 | mkdir -p usr/share/metainfo 52 | for f in ${target}/share/appdata/*.xml; do 53 | ln -s ../appdata/$(basename $f) usr/share/metainfo/$(basename $f) 54 | done 55 | fi 56 | 57 | # .desktop 58 | desktop=$(find ${target}/share/applications -name "*.desktop" | head -n1) 59 | if ! [ -z "$desktop" ]; then 60 | cp .$desktop . 61 | fi 62 | 63 | 64 | # icons 65 | if [ -d ${target}/share/icons ]; then 66 | icon=$(find ${target}/share/icons -name "${name}*.png" | head -n1) 67 | if ! [ -z "$icon" ]; then 68 | ln -s .$icon 69 | ln -s .$icon .DirIcon 70 | else 71 | icon=$(find ${target}/share/icons -name "${name}*.svg" | head -n1) 72 | if ! [ -z "$icon" ]; then 73 | ln -s .$icon 74 | ln -s .$icon .DirIcon 75 | fi 76 | fi 77 | fi 78 | 79 | cp ${AppRun targets}/bin/AppRun AppRun 80 | ''; 81 | } 82 | -------------------------------------------------------------------------------- /appdir.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | if [ "$#" -lt 1 ]; then 4 | cat <{}; writers.writePython3Bin "helloThere.py" {} "print(1)\n"' --argstr exec helloThere.py 4 | 5 | 6 | {nixpkgs ? import {}, 7 | package, 8 | exec, 9 | ... }: 10 | let 11 | appimage_src = drv : exec : with nixpkgs; 12 | self.stdenv.mkDerivation rec { 13 | name = drv.name + "-appdir"; 14 | env = buildEnv { 15 | inherit name; 16 | paths = buildInputs; 17 | }; 18 | src = env; 19 | inherit exec; 20 | buildInputs = [ drv ]; 21 | buildCommand = '' 22 | mkdir -p $out/share/icons/hicolor/256x256/apps 23 | mkdir -p $out/share/applications 24 | 25 | shopt -s extglob 26 | ln -s ${env}/!(share) $out/ 27 | ln -s ${env}/share/* $out/share/ 28 | 29 | touch $out/share/icons/hicolor/256x256/apps/${drv.name}.png 30 | touch $out/share/icons/${drv.name}.png 31 | 32 | cat < $out/share/applications/${drv.name}.desktop 33 | [Desktop Entry] 34 | Type=Application 35 | Version=1.0 36 | Name=${drv.name} 37 | Path=${env} 38 | Icon=${drv.name} 39 | Exec=$exec 40 | Terminal=true 41 | EOF 42 | ''; 43 | system = builtins.currentSystem; 44 | }; 45 | 46 | in 47 | let results = 48 | if (nixpkgs.lib.isDerivation package && !(nixpkgs.lib.isString package)) 49 | then { 50 | name = package.name; 51 | target = appimage_src package "${exec}"; 52 | extraTargets = []; 53 | } 54 | else { 55 | name = nixpkgs."${package}".name; 56 | target = appimage_src (nixpkgs."${package}") "${exec}"; 57 | extraTargets = []; 58 | }; 59 | in 60 | with (import (./appimage-top.nix){nixpkgs' = nixpkgs.path;}); 61 | (appimage (appdir results )).overrideAttrs (old: {name = results.name;}) 62 | -------------------------------------------------------------------------------- /appimage-top.nix: -------------------------------------------------------------------------------- 1 | { nixpkgs' ? }: 2 | 3 | let 4 | pkgs = import nixpkgs' { }; 5 | muslPkgs = import nixpkgs' { 6 | localSystem.config = "x86_64-unknown-linux-musl"; 7 | }; 8 | 9 | in rec { 10 | appimagetool = pkgs.callPackage ./appimagetool.nix {}; 11 | 12 | appimage = pkgs.callPackage ./appimage.nix { 13 | inherit appimagetool; 14 | }; 15 | 16 | appdir = pkgs.callPackage ./appdir.nix { inherit muslPkgs; }; 17 | } 18 | -------------------------------------------------------------------------------- /appimage.nix: -------------------------------------------------------------------------------- 1 | { stdenv, appimagetool }: 2 | dir: 3 | 4 | stdenv.mkDerivation { 5 | name = "appimage"; 6 | buildInputs = [ appimagetool ]; 7 | buildCommand = '' 8 | ARCH=x86_64 appimagetool ${dir}/*.AppDir 9 | mkdir $out 10 | cp *.AppImage $out 11 | ''; 12 | } 13 | -------------------------------------------------------------------------------- /appimagetool.nix: -------------------------------------------------------------------------------- 1 | { stdenv, lib, fetchurl, fuse, zlib, squashfsTools, glib }: 2 | 3 | # This is from some binaries. 4 | 5 | # Ideally, this should be source based, 6 | # but I can't get it to build from GitHub 7 | 8 | stdenv.mkDerivation rec { 9 | name = "appimagekit"; 10 | 11 | src = fetchurl { 12 | url = "https://github.com/AppImage/AppImageKit/releases/download/10/appimagetool-x86_64.AppImage"; 13 | sha256 = "03zbiblj8a1yk1xsb5snxi4ckwn3diyldg1jh5hdjjhsmpw652ig"; 14 | }; 15 | 16 | buildInputs = [ 17 | squashfsTools 18 | ]; 19 | 20 | sourceRoot = "squashfs-root"; 21 | 22 | unpackPhase = '' 23 | cp $src appimagetool-x86_64.AppImage 24 | chmod u+wx appimagetool-x86_64.AppImage 25 | patchelf \ 26 | --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \ 27 | --set-rpath ${fuse}/lib:${zlib}/lib \ 28 | appimagetool-x86_64.AppImage 29 | 30 | ./appimagetool-x86_64.AppImage --appimage-extract 31 | ''; 32 | 33 | installPhase = '' 34 | mkdir -p $out 35 | cp -r usr/* $out 36 | 37 | for x in $out/bin/*; do 38 | patchelf \ 39 | --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \ 40 | --set-rpath ${lib.makeLibraryPath [ zlib stdenv.cc.libc fuse glib ]} \ 41 | $x 42 | done 43 | ''; 44 | 45 | dontStrip = true; 46 | dontPatchELF = true; 47 | } 48 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | {nixpkgs ? import {}}: 2 | 3 | with nixpkgs; 4 | 5 | rec { 6 | toStorePath = target: 7 | # If a store path has been given but is not a derivation, add the missing context 8 | # to it so it will be propagated properly as a build input. 9 | if !(lib.isDerivation target) && lib.isStorePath target then 10 | let path = toString target; in 11 | builtins.appendContext path { "${path}" = { path = true; }; } 12 | # Otherwise, add to the store. This takes care of appending the store path 13 | # in the context automatically. 14 | else "${target}"; 15 | 16 | arx = { drvToBundle, archive, startup}: 17 | stdenv.mkDerivation { 18 | name = if drvToBundle != null then "${drvToBundle.pname}-arx" else "arx"; 19 | passthru = { 20 | inherit drvToBundle; 21 | }; 22 | buildCommand = '' 23 | # tmpdir has a additional `/` in the beginning to work around `QualifiedPath` checking for `|/|./|../|` 24 | ${haskellPackages.arx}/bin/arx tmpx \ 25 | --tmpdir '/$HOME/.cache' \ 26 | --shared \ 27 | -rm! ${archive} \ 28 | -o $out // ${startup} 29 | chmod +x $out 30 | ''; 31 | }; 32 | 33 | maketar = { targets }: 34 | stdenv.mkDerivation { 35 | name = "maketar"; 36 | buildInputs = [ perl ]; 37 | exportReferencesGraph = map (x: [("closure-" + baseNameOf x) x]) targets; 38 | buildCommand = '' 39 | storePaths=$(perl ${pathsFromGraph} ./closure-*) 40 | 41 | # https://reproducible-builds.org/docs/archives 42 | tar -cf - \ 43 | --owner=0 --group=0 --mode=u+rw,uga+r \ 44 | --hard-dereference \ 45 | --mtime="@$SOURCE_DATE_EPOCH" \ 46 | --format=gnu \ 47 | --sort=name \ 48 | $storePaths | bzip2 -z > $out 49 | ''; 50 | }; 51 | 52 | # TODO: eventually should this go in nixpkgs? 53 | nix-user-chroot = lib.makeOverridable stdenv.mkDerivation { 54 | name = "nix-user-chroot-2c52b5f"; 55 | src = ./nix-user-chroot; 56 | 57 | buildInputs = [ 58 | stdenv.cc.cc.libgcc or null 59 | ]; 60 | 61 | makeFlags = []; 62 | 63 | # hack to use when /nix/store is not available 64 | postFixup = '' 65 | exe=$out/bin/nix-user-chroot 66 | patchelf \ 67 | --set-interpreter .$(patchelf --print-interpreter $exe) \ 68 | --set-rpath $(patchelf --print-rpath $exe | sed 's|/nix/store/|./nix/store/|g') \ 69 | $exe 70 | ''; 71 | 72 | installPhase = '' 73 | runHook preInstall 74 | 75 | mkdir -p $out/bin/ 76 | cp nix-user-chroot $out/bin/nix-user-chroot 77 | 78 | runHook postInstall 79 | ''; 80 | 81 | meta.platforms = lib.platforms.linux; 82 | }; 83 | 84 | makebootstrap = { targets, startup, drvToBundle ? null }: 85 | arx { 86 | inherit drvToBundle startup; 87 | archive = maketar { 88 | inherit targets; 89 | }; 90 | }; 91 | 92 | makeStartup = { target, nixUserChrootFlags, nix-user-chroot', run, initScript }: 93 | let 94 | # Avoid re-adding a store path into the store 95 | path = toStorePath target; 96 | in 97 | writeScript "startup" '' 98 | #!/bin/sh 99 | ${initScript} 100 | .${nix-user-chroot'}/bin/nix-user-chroot -n ./nix ${nixUserChrootFlags} -- ${path}${run} "$@" 101 | ''; 102 | 103 | nix-bootstrap = { target, extraTargets ? [], run, nix-user-chroot' ? nix-user-chroot, nixUserChrootFlags ? "", initScript ? "" }: 104 | let 105 | script = makeStartup { inherit target nixUserChrootFlags nix-user-chroot' run initScript; }; 106 | in makebootstrap { 107 | startup = ".${script} '\"$@\"'"; 108 | targets = [ "${script}" ] ++ extraTargets; 109 | }; 110 | 111 | nix-bootstrap-nix = {target, run, extraTargets ? []}: 112 | nix-bootstrap-path { 113 | inherit target run; 114 | extraTargets = [ gnutar bzip2 xz gzip coreutils bash ] ++ extraTargets; 115 | }; 116 | 117 | # special case adding path to the environment before launch 118 | nix-bootstrap-path = let 119 | nix-user-chroot'' = targets: nix-user-chroot.overrideDerivation (o: { 120 | buildInputs = o.buildInputs ++ targets; 121 | makeFlags = o.makeFlags ++ [ 122 | ''ENV_PATH="${lib.makeBinPath targets}"'' 123 | ]; 124 | }); in { target, extraTargets ? [], run, initScript ? "" }: nix-bootstrap { 125 | inherit target extraTargets run initScript; 126 | nix-user-chroot' = nix-user-chroot'' extraTargets; 127 | }; 128 | } 129 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1728492678, 6 | "narHash": "sha256-9UTxR8eukdg+XZeHgxW5hQA9fIKHsKCdOIUycTryeVw=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "5633bcff0c6162b9e4b5f1264264611e950c8ec7", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs", 22 | "utils": "utils" 23 | } 24 | }, 25 | "systems": { 26 | "locked": { 27 | "lastModified": 1681028828, 28 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 29 | "owner": "nix-systems", 30 | "repo": "default", 31 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 32 | "type": "github" 33 | }, 34 | "original": { 35 | "owner": "nix-systems", 36 | "repo": "default", 37 | "type": "github" 38 | } 39 | }, 40 | "utils": { 41 | "inputs": { 42 | "systems": "systems" 43 | }, 44 | "locked": { 45 | "lastModified": 1726560853, 46 | "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", 47 | "owner": "numtide", 48 | "repo": "flake-utils", 49 | "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "numtide", 54 | "repo": "flake-utils", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "The purely functional package manager"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | utils.url = "github:numtide/flake-utils"; 7 | }; 8 | 9 | outputs = 10 | inputs: 11 | let 12 | inherit (inputs.nixpkgs) lib; 13 | 14 | getExe = 15 | x: 16 | lib.getExe' x ( 17 | x.meta.mainProgram or (lib.warn 18 | "nix-bundle: Package ${ 19 | lib.strings.escapeNixIdentifier x.meta.name or x.pname or x.name 20 | } does not have the meta.mainProgram attribute. Assuming you want '${lib.getName x}'." 21 | lib.getName 22 | x 23 | ) 24 | ); 25 | in 26 | inputs.utils.lib.eachDefaultSystem ( 27 | system: 28 | let 29 | nix-bundle-fun = 30 | { 31 | drv, 32 | programPath ? getExe drv, 33 | }: 34 | let 35 | nixpkgs = inputs.nixpkgs.legacyPackages.${system}; 36 | nix-bundle = import inputs.self { inherit nixpkgs; }; 37 | script = nixpkgs.writeScript "startup" '' 38 | #!/bin/sh 39 | .${nix-bundle.nix-user-chroot}/bin/nix-user-chroot -n ./nix -- ${programPath} "$@" 40 | ''; 41 | in 42 | nix-bundle.makebootstrap { 43 | drvToBundle = drv; 44 | targets = [ script ]; 45 | startup = ".${builtins.unsafeDiscardStringContext script} '\"$@\"'"; 46 | }; 47 | in 48 | { 49 | bundlers = { 50 | default = inputs.self.bundlers.${system}.nix-bundle; 51 | nix-bundle = drv: nix-bundle-fun { inherit drv; }; 52 | }; 53 | } 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /install-nix-from-closure.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | dest="/nix" 6 | self="." 7 | nix="@nix@" 8 | cacert="@cacert@" 9 | 10 | if ! [ -e $self/.reginfo ]; then 11 | echo "$0: incomplete installer (.reginfo is missing)" >&2 12 | exit 1 13 | fi 14 | 15 | if [ -z "$USER" ]; then 16 | echo "$0: \$USER is not set" >&2 17 | exit 1 18 | fi 19 | 20 | if [ "$(id -u)" -eq 0 ]; then 21 | printf '\e[1;31mwarning: installing Nix as root is not supported by this script!\e[0m\n' 22 | fi 23 | 24 | echo "performing a single-user installation of Nix..." >&2 25 | 26 | if ! [ -e $dest ]; then 27 | cmd="mkdir -m 0755 $dest && chown $USER $dest" 28 | echo "directory $dest does not exist; creating it by running '$cmd' using sudo" >&2 29 | if ! sudo sh -c "$cmd"; then 30 | echo "$0: please manually run ‘$cmd’ as root to create $dest" >&2 31 | exit 1 32 | fi 33 | fi 34 | 35 | if ! [ -w $dest ]; then 36 | echo "$0: directory $dest exists, but is not writable by you. This could indicate that another user has already performed a single-user installation of Nix on this system. If you wish to enable multi-user support see http://nixos.org/nix/manual/#ssec-multi-user. If you wish to continue with a single-user install for $USER please run ‘chown -R $USER $dest’ as root." >&2 37 | exit 1 38 | fi 39 | 40 | mkdir -p $dest/store 41 | 42 | echo -n "copying Nix to $dest/store..." >&2 43 | 44 | for i in $(cd $self/store >/dev/null && echo *); do 45 | echo -n "." >&2 46 | i_tmp="$dest/store/$i.$$" 47 | if [ -e "$i_tmp" ]; then 48 | rm -rf "$i_tmp" 49 | fi 50 | if ! [ -e "$dest/store/$i" ]; then 51 | cp -Rp "$self/store/$i" "$i_tmp" 52 | chmod -R a-w "$i_tmp" 53 | chmod +w "$i_tmp" 54 | mv "$i_tmp" "$dest/store/$i" 55 | chmod -w "$dest/store/$i" 56 | fi 57 | done 58 | echo "" >&2 59 | 60 | echo "initialising Nix database..." >&2 61 | if ! $nix/bin/nix-store --init; then 62 | echo "$0: failed to initialize the Nix database" >&2 63 | exit 1 64 | fi 65 | 66 | if ! $nix/bin/nix-store --load-db < $self/.reginfo; then 67 | echo "$0: unable to register valid paths" >&2 68 | exit 1 69 | fi 70 | 71 | . $nix/etc/profile.d/nix.sh 72 | 73 | if ! $nix/bin/nix-env -i "$nix"; then 74 | echo "$0: unable to install Nix into your default profile" >&2 75 | exit 1 76 | fi 77 | 78 | # Install an SSL certificate bundle. 79 | if [ -z "$SSL_CERT_FILE" -o ! -f "$SSL_CERT_FILE" ]; then 80 | $nix/bin/nix-env -i "$cacert" 81 | export SSL_CERT_FILE="$HOME/.nix-profile/etc/ssl/certs/ca-bundle.crt" 82 | fi 83 | 84 | # Subscribe the user to the Nixpkgs channel and fetch it. 85 | if ! $nix/bin/nix-channel --list | grep -q "^nixpkgs "; then 86 | $nix/bin/nix-channel --add https://nixos.org/channels/nixpkgs-unstable 87 | fi 88 | if [ -z "$_NIX_INSTALLER_TEST" ]; then 89 | $nix/bin/nix-channel --update nixpkgs 90 | fi 91 | 92 | # Make the shell source nix.sh during login. 93 | p=$HOME/.nix-profile/etc/profile.d/nix.sh 94 | 95 | added= 96 | for i in .bash_profile .bash_login .profile; do 97 | fn="$HOME/$i" 98 | if [ -w "$fn" ]; then 99 | if ! grep -q "$p" "$fn"; then 100 | echo "modifying $fn..." >&2 101 | echo "if [ -e $p ]; then . $p; fi # added by Nix installer" >> $fn 102 | fi 103 | added=1 104 | break 105 | fi 106 | done 107 | 108 | if [ -z "$added" ]; then 109 | cat >&2 <&2 <&2 echo "$0 failed. Exiting." 56 | exit 1 57 | elif [ -t 1 ]; then 58 | filename=$(basename "$exec") 59 | echo "Nix bundle created at $filename." 60 | cp -f "$out" "$filename" 61 | else 62 | echo "$out" 63 | fi 64 | -------------------------------------------------------------------------------- /nix-installer.nix: -------------------------------------------------------------------------------- 1 | { stdenv, fetchFromGitHub, writeText, nix, cacert }: 2 | 3 | stdenv.mkDerivation { 4 | name = "nix-installer"; 5 | 6 | propagatedBuildInputs = [ nix.out cacert ]; 7 | 8 | buildCommand = '' 9 | mkdir -p $out/bin/ 10 | substitute ${./install-nix-from-closure.sh} $out/install \ 11 | --subst-var-by nix ${nix.out} \ 12 | --subst-var-by cacert ${cacert} 13 | chmod +x $out/install 14 | ''; 15 | } 16 | -------------------------------------------------------------------------------- /nix-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # nix-run.sh provides an easy way to run executables from Nix derivations 4 | # without installing them. It will try to determine how to run the application 5 | # based on what files are installable. Currently, macOS apps, Freedesktop apps, 6 | # and ordinary binaries are handled. 7 | 8 | # Usage 9 | 10 | if [ -z "$1" ]; then 11 | >&2 echo "Need more than one argument." 12 | >&2 echo 13 | >&2 echo "Try:" 14 | >&2 echo "$ nix-run hello" 15 | >&2 echo 16 | >&2 echo "To run the hello program" 17 | >&2 echo "or substitute hello with another package in Nixpkgs" 18 | exit 1 19 | fi 20 | 21 | pkg="$1" 22 | shift 23 | 24 | # A second argument will provide a hint to run 25 | if [ -n "$1" ]; then 26 | name="$1" 27 | shift 28 | else 29 | name="$pkg" 30 | fi 31 | 32 | expr="with import {}; let x = ($pkg); in x" 33 | path=$(nix-instantiate --no-gc-warning -E "$expr") 34 | out=$(nix-store --no-gc-warning -r "$path") 35 | 36 | if [ -z "$out" ]; then 37 | >&2 echo "Could not evaluate $pkg to a Nix drv." 38 | exit 1 39 | fi 40 | 41 | # Run DIR as a Darwin application 42 | run_darwin_app () { 43 | dir="$1" 44 | shift 45 | 46 | open -a "$dir" --args "$@" 47 | } 48 | 49 | # Run FILE as a Freedesktop application 50 | # taken from: 51 | # https://askubuntu.com/questions/5172/running-a-desktop-file-in-the-terminal/5174 52 | run_linux_desktop_app () { 53 | file="$1" 54 | shift 55 | 56 | cmd=$(grep '^Exec' "$file" | tail -1 | \ 57 | sed 's/Exec=//;s/^"//;s/" *$//') 58 | 59 | if [ "$#" -gt 0 ]; then 60 | cmd=$(echo "$cmd" | sed "s/%[fu]/$1/;s/%[FU]/$*/") 61 | fi 62 | 63 | cmd=$(echo "$cmd" | sed "s/%k/$desktop/;s/%.//") 64 | 65 | "$cmd" "$@" 66 | } 67 | 68 | # Run FILE as an ordinary binary 69 | run_bin () { 70 | file="$1" 71 | shift 72 | 73 | "$file" "$@" 74 | } 75 | 76 | if [ -x "$out/nix-support/run" ]; then 77 | run_bin "$out/nix-support/run" "$@" 78 | elif [ -x "$out/bin/run" ]; then 79 | run_bin "$out/bin/run" "$@" 80 | elif [ "$(uname)" = Darwin ] && [ -d "$out/Applications/$name.app" ]; then 81 | run_darwin_app "$out/Applications/$name.app" "$@" 82 | elif [ "$(uname)" = Darwin ] && [ -d "$out"/Applications/*.app ]; then 83 | for f in "$out"/Applications/*.app; do 84 | run_darwin_app "$f" "$@" 85 | done 86 | elif [ -f "$out/share/applications/$name.desktop" ]; then 87 | run_linux_desktop_app "$out/share/applications/$name.desktop" "$@" 88 | elif [ -d "$out"/share/applications ]; then 89 | for f in "$out"/share/applications/*.desktop; do 90 | run_linux_desktop_app "$f" 91 | done 92 | elif [ -x "$out/bin/$name" ]; then 93 | run_bin "$out/bin/$name" "$@" 94 | elif [ -d "$out/bin" ]; then 95 | for bin in "$out"/bin/*; do 96 | run_bin "$bin" "$@" 97 | done 98 | else 99 | >&2 echo "Cannot find a way to run path $out." 100 | exit 1 101 | fi 102 | -------------------------------------------------------------------------------- /nix-strace.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | pkg="$1" 4 | shift 5 | exe="$1" 6 | shift 7 | bin=$(mktemp) 8 | out=$(mktemp) 9 | ~/nix.sh ./nix-bundle.sh $pkg $exe > $bin 10 | chmod +x $bin 11 | strace -f -o $out $bin $@ 12 | cat $out | grep -E '^[0-9]+ open\("\.?\/nix' | grep -Ev " = -[0-9]+ [A-Z]+ \([a-zA-Z ]+\)$" | sed -E 's/^[0-9]+ open\("\.?([^\"]+)".*/\1/' 13 | -------------------------------------------------------------------------------- /nix-user-chroot/Makefile: -------------------------------------------------------------------------------- 1 | ENV_PATH ?= "" 2 | 3 | nix-user-chroot: main.cpp 4 | ${CXX} -o nix-user-chroot -DENV_PATH='$(ENV_PATH)' main.cpp 5 | -------------------------------------------------------------------------------- /nix-user-chroot/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is based on @lethalman's nix-user-chroot. This file has 3 | * diverged from it though. 4 | * 5 | * Usage: nix-user-chroot 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | using namespace std; 27 | 28 | #define err_exit(format, ...) { fprintf(stderr, format ": %s\n", ##__VA_ARGS__, strerror(errno)); exit(EXIT_FAILURE); } 29 | static int child_proc(const char *rootdir, const char *nixdir, uint8_t clear_env, list dirMappings, list envMappings, const char *executable, char * const new_argv[]); 30 | 31 | volatile uint8_t child_died = 0; 32 | int child_pid = 0; 33 | 34 | static void usage(const char *pname) { 35 | fprintf(stderr, "Usage: %s -n -- \n", pname); 36 | fprintf(stderr, "\t-c\tclear all env vars\n"); 37 | fprintf(stderr, "\t-m :\tmap src on the host to dest in the sandbox\n"); 38 | fprintf(stderr, "\t-d\tdelete all default dir mappings, may break things\n"); 39 | fprintf(stderr, "\t-p \tpreserve the value of a variable across the -c clear\n"); 40 | fprintf(stderr, "\t-e\tadd an /escape-hatch to the sandbox, and run (outside the sandbox) any strings written to it\n"); 41 | 42 | exit(EXIT_FAILURE); 43 | } 44 | 45 | static void update_map(const char *mapping, const char *map_file) { 46 | int fd; 47 | 48 | fd = open(map_file, O_WRONLY); 49 | if (fd < 0) { 50 | err_exit("map open"); 51 | } 52 | 53 | int map_len = strlen(mapping); 54 | if (write(fd, mapping, map_len) != map_len) { 55 | err_exit("map write"); 56 | } 57 | 58 | close(fd); 59 | } 60 | 61 | static void add_path(string src, string dest, string rootdir) { 62 | string path_buf2; 63 | 64 | struct stat statbuf; 65 | if (stat(src.c_str(), &statbuf) < 0) { 66 | fprintf(stderr, "Cannot stat %s: %s\n", src.c_str(), strerror(errno)); 67 | return; 68 | } 69 | 70 | path_buf2 = rootdir + "/" + dest; 71 | 72 | if (S_ISDIR(statbuf.st_mode)) { 73 | mkdir(path_buf2.c_str(), statbuf.st_mode & ~S_IFMT); 74 | if (mount(src.c_str(), path_buf2.c_str(), "none", MS_BIND | MS_REC, NULL) < 0) { 75 | fprintf(stderr, "Cannot bind mount %s to %s: %s\n", src.c_str(), path_buf2.c_str(), strerror(errno)); 76 | } 77 | } else if (S_ISREG(statbuf.st_mode)) { 78 | printf("bind-mounting file %s not supported", src.c_str()); 79 | } 80 | } 81 | 82 | struct DirMapping { 83 | string src; 84 | string dest; 85 | }; 86 | 87 | struct SetEnv { 88 | string key; 89 | string value; 90 | }; 91 | 92 | struct DirMapping parseMapping(string input) { 93 | auto pos = input.find(":"); 94 | string src = input.substr(0, pos); 95 | string dest = input.substr(pos + 1); 96 | return (struct DirMapping){ src, dest }; 97 | } 98 | 99 | static void handle_child_death(int signo, siginfo_t *info, void *context) { 100 | if ( (child_pid == 0) || (info->si_pid == child_pid) ) { 101 | child_died = 1; 102 | } 103 | } 104 | 105 | int main(int argc, char *argv[]) { 106 | uint8_t clear_env = 0; 107 | uint8_t enable_escape_hatch = 0; 108 | char *nixdir = NULL; 109 | list dirMappings; 110 | list envMappings; 111 | const char *t; 112 | 113 | #define x(y) dirMappings.push_back({ "/" y, y }) 114 | x("dev"); 115 | x("proc"); 116 | x("sys"); 117 | x("run"); 118 | x("tmp"); 119 | x("var"); 120 | x("etc"); 121 | x("usr"); 122 | x("home"); 123 | x("root"); 124 | #undef x 125 | 126 | int opt; 127 | while ((opt = getopt(argc, argv, "cen:m:dp:")) != -1) { 128 | switch (opt) { 129 | case 'c': 130 | clear_env = 1; 131 | break; 132 | case 'e': 133 | enable_escape_hatch = 1; 134 | break; 135 | case 'n': 136 | // determine absolute directory for nix dir 137 | nixdir = realpath(optarg, NULL); 138 | if (!nixdir) { 139 | err_exit("realpath(%s)", optarg); 140 | } 141 | break; 142 | case 'm': 143 | dirMappings.push_back(parseMapping(optarg)); 144 | break; 145 | case 'd': 146 | dirMappings.clear(); 147 | break; 148 | case 'p': 149 | t = getenv(optarg); 150 | if (t) { 151 | envMappings.push_back({ optarg, t }); 152 | } 153 | break; 154 | } 155 | } 156 | 157 | if (!nixdir) { 158 | fprintf(stderr, "-n is required\n"); 159 | exit(EXIT_FAILURE); 160 | } 161 | 162 | if (argc <= optind) { 163 | usage(argv[0]); 164 | } 165 | 166 | const char *tmpdir = getenv("TMPDIR"); 167 | if (!tmpdir) { 168 | tmpdir = "/tmp"; 169 | } 170 | 171 | char template_[PATH_MAX]; 172 | int needed = snprintf(template_, PATH_MAX, "%s/nixXXXXXX", tmpdir); 173 | if (needed < 0) { 174 | err_exit("TMPDIR too long: '%s'", tmpdir); 175 | } 176 | 177 | char *rootdir = mkdtemp(template_); 178 | if (!rootdir) { 179 | err_exit("mkdtemp(%s)", template_); 180 | } 181 | 182 | int unrace[2]; 183 | 184 | if (pipe(unrace)) { 185 | err_exit("pipe()"); 186 | } 187 | 188 | struct sigaction handle_child; 189 | handle_child.sa_sigaction = handle_child_death; 190 | handle_child.sa_flags = SA_SIGINFO; 191 | 192 | struct sigaction old_handler; 193 | if (sigaction(SIGCHLD, &handle_child, &old_handler)) { 194 | err_exit("sigaction()"); 195 | } 196 | 197 | int child; 198 | child = child_pid = fork(); 199 | if (child < 0) { 200 | err_exit("fork()"); 201 | } else if (child == 0) { 202 | sigaction(SIGCHLD, &old_handler, NULL); 203 | close(unrace[1]); 204 | char buf[10]; 205 | read(unrace[0], buf, 10); 206 | close(unrace[0]); 207 | return child_proc(rootdir, nixdir, clear_env, dirMappings, envMappings, argv[optind], argv + optind); 208 | } else { 209 | close(unrace[0]); 210 | char fifopath[PATH_MAX]; 211 | if (enable_escape_hatch) { 212 | snprintf(fifopath, PATH_MAX, "%s/escape-hatch", rootdir); 213 | mkfifo(fifopath, 0600); 214 | } 215 | close(unrace[1]); 216 | if (enable_escape_hatch) { 217 | char buffer[1024]; 218 | while (!child_died) { 219 | int fd = open(fifopath, O_RDONLY); 220 | if (fd < 0) { 221 | if (errno == EINTR) continue; 222 | fprintf(stderr, "error opening escape-hatch: %s\n", strerror(errno)); 223 | continue; 224 | } 225 | int size = read(fd, buffer, 1024); 226 | buffer[size] = 0; 227 | system(buffer); 228 | close(fd); 229 | } 230 | } 231 | int status; 232 | int ret = waitpid(child, &status, 0); 233 | return WEXITSTATUS(status); 234 | } 235 | } 236 | 237 | static int child_proc(const char *rootdir, const char *nixdir, uint8_t clear_env, list dirMappings, list envMappings, const char *executable, char * const new_argv[]) { 238 | // get uid, gid before going to new namespace 239 | uid_t uid = getuid(); 240 | gid_t gid = getgid(); 241 | 242 | // "unshare" into new namespace 243 | if (unshare(CLONE_NEWNS | CLONE_NEWUSER) < 0) { 244 | if (errno == EPERM) { 245 | fputs("Run the following to enable unprivileged namespace use:\nsudo bash -c \"sysctl -w kernel.unprivileged_userns_clone=1 ; echo kernel.unprivileged_userns_clone=1 > /etc/sysctl.d/nix-user-chroot.conf\"\n\n", stderr); 246 | exit(EXIT_FAILURE); 247 | } else { 248 | err_exit("unshare()"); 249 | } 250 | } 251 | 252 | // add necessary system stuff to rootdir namespace 253 | for (list::iterator it = dirMappings.begin(); 254 | it != dirMappings.end(); ++it) { 255 | struct DirMapping m = *it; 256 | add_path(m.src, m.dest, rootdir); 257 | } 258 | 259 | // make sure nixdir exists 260 | struct stat statbuf2; 261 | if (stat(nixdir, &statbuf2) < 0) { 262 | err_exit("stat(%s)", nixdir); 263 | } 264 | 265 | char path_buf[PATH_MAX]; 266 | // mount /nix to new namespace 267 | snprintf(path_buf, sizeof(path_buf), "%s/nix", rootdir); 268 | mkdir(path_buf, statbuf2.st_mode & ~S_IFMT); 269 | if (mount(nixdir, path_buf, "none", MS_BIND | MS_REC, NULL) < 0) { 270 | err_exit("mount(%s, %s)", nixdir, path_buf); 271 | } 272 | 273 | // fixes issue #1 where writing to /proc/self/gid_map fails 274 | // see user_namespaces(7) for more documentation 275 | int fd_setgroups = open("/proc/self/setgroups", O_WRONLY); 276 | if (fd_setgroups > 0) { 277 | write(fd_setgroups, "deny", 4); 278 | close(fd_setgroups); 279 | } 280 | 281 | // map the original uid/gid in the new ns 282 | char map_buf[1024]; 283 | snprintf(map_buf, sizeof(map_buf), "%d %d 1", uid, uid); 284 | update_map(map_buf, "/proc/self/uid_map"); 285 | snprintf(map_buf, sizeof(map_buf), "%d %d 1", gid, gid); 286 | update_map(map_buf, "/proc/self/gid_map"); 287 | 288 | // chroot to rootdir 289 | if (chroot(rootdir) < 0) { 290 | err_exit("chroot(%s)", rootdir); 291 | } 292 | 293 | chdir("/"); 294 | 295 | if (clear_env) clearenv(); 296 | setenv("PATH", ENV_PATH, 1); 297 | 298 | for (list::iterator it = envMappings.begin(); 299 | it != envMappings.end(); ++it) { 300 | struct SetEnv e = *it; 301 | setenv(e.key.c_str(), e.value.c_str(), 1); 302 | } 303 | 304 | // execute the command 305 | execvp(executable, new_argv); 306 | err_exit("execvp(%s)", executable); 307 | } 308 | -------------------------------------------------------------------------------- /nix2appimage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | if [ "$#" -lt 1 ]; then 4 | cat < 2 | , nixpkgs' ? import nixpkgs {}}: with nixpkgs'; 3 | 4 | stdenv.mkDerivation rec { 5 | pname = "nix-bundle"; 6 | name = "${pname}-${version}"; 7 | version = "0.4.0"; 8 | 9 | src = ./.; 10 | 11 | # coreutils, gnutar is actually needed by nix for bootstrap 12 | buildInputs = [ nix coreutils makeWrapper gnutar gzip bzip2 ]; 13 | 14 | nixBundlePath = lib.makeBinPath [ nix coreutils gnutar gzip bzip2 ]; 15 | nixRunPath = lib.makeBinPath [ nix coreutils ]; 16 | 17 | makeFlags = [ "PREFIX=$(out)" ]; 18 | 19 | postInstall = '' 20 | mkdir -p $out/bin 21 | makeWrapper $out/share/nix-bundle/nix-bundle.sh $out/bin/nix-bundle \ 22 | --prefix PATH : ${nixBundlePath} 23 | makeWrapper $out/share/nix-bundle/nix-run.sh $out/bin/nix-run \ 24 | --prefix PATH : ${nixRunPath} 25 | ''; 26 | 27 | meta = with lib; { 28 | maintainers = [ maintainers.matthewbauer ]; 29 | platforms = platforms.all; 30 | description = "Create bundles from Nixpkgs attributes"; 31 | license = licenses.mit; 32 | homepage = https://github.com/matthewbauer/nix-bundle; 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /test-appimage.nix: -------------------------------------------------------------------------------- 1 | { appimagefile, nixpkgs' ? }: 2 | 3 | # nix build -f test-appimage.nix --arg appimagefile ./VLC*AppImage 4 | 5 | with import nixpkgs' {}; 6 | 7 | runCommandCC "patchelf" {} '' 8 | cp ${appimagefile} $out 9 | chmod +w $out 10 | patchelf \ 11 | --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \ 12 | --set-rpath ${lib.makeLibraryPath [ stdenv.cc.libc fuse zlib glib ]} 13 | $out 14 | '' 15 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | export NIX_PATH=channel:nixos-20.09 4 | 5 | echo "Test with attribute name" 6 | ./nix-bundle.sh hello /bin/hello 7 | 8 | echo "Test with store path" 9 | out=$(nix-build --no-out-link --expr '(import {})' -A hello) 10 | ./nix-bundle.sh "$out" /bin/hello 11 | --------------------------------------------------------------------------------