diff --git a/lib/create-darwin-volume.sh b/lib/create-darwin-volume.sh new file mode 100755 index 0000000..03bf05b --- /dev/null +++ b/lib/create-darwin-volume.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +set -e + +root_disks() { + diskutil list -plist / +} + +apfs_volumes_for() { + disk=$1 + diskutil apfs list -plist "$disk" +} + +disk_identifier() { + xpath "/plist/dict/key[text()='WholeDisks']/following-sibling::array[1]/string/text()" 2>/dev/null +} + +volume_get() { + key=$1 i=$2 + xpath "/plist/dict/array/dict/key[text()='Volumes']/following-sibling::array/dict[$i]/key[text()='$key']/following-sibling::string[1]/text()" 2> /dev/null +} + +find_nix_volume() { + disk=$1 + i=1 + volumes=$(apfs_volumes_for "$disk") + while true; do + name=$(echo "$volumes" | volume_get "Name" "$i") + if [ -z "$name" ]; then + break + fi + case "$name" in + [Nn]ix*) + echo "$name" + break + ;; + esac + i=$((i+1)) + done +} + +test_fstab() { + grep -q "/nix" /etc/fstab 2>/dev/null +} + +test_synthetic_conf() { + grep -q "^nix" /etc/synthetic.conf 2>/dev/null +} + +test_nix() { + test -d "/nix" +} + +main() { + ( + echo "" + echo " ------------------------------------------------------------------ " + echo " | This installer will create a volume for the nix store and |" + echo " | configure it to mount at /nix. Follow these steps to uninstall. |" + echo " ------------------------------------------------------------------ " + echo "" + echo " 1. Remove the entry from fstab using 'sudo vifs'" + echo " 2. Destroy the data volume using 'diskutil apfs deleteVolume'" + echo " 3. Delete /etc/synthetic.conf" + echo "" + ) >&2 + + if [ -L "/nix" ]; then + echo "error: /nix is a symlink, please remove it or edit synthetic.conf (requires reboot)" >&2 + echo " /nix -> $(readlink "/nix")" >&2 + exit 2 + fi + + if ! test_synthetic_conf; then + echo "Configuring /etc/synthetic.conf..." >&2 + echo nix | sudo tee /etc/synthetic.conf + /System/Library/Filesystems/apfs.fs/Contents/Resources/apfs.util -B + fi + + if ! test_nix; then + echo "Creating mountpoint for /nix..." >&2 + sudo mkdir /nix + fi + + disk=$(root_disks | disk_identifier) + volume=$(find_nix_volume "$disk") + if [ -z "$volume" ]; then + echo "Creating a Nix Store volume..." >&2 + sudo diskutil apfs addVolume "$disk" APFS 'Nix Store' -mountpoint /nix + volume="Nix Store" + else + echo "Using existing '$volume' volume" >&2 + fi + + if ! test_fstab; then + echo "Configuring /etc/fstab..." >&2 + label=$(echo "$volume" | sed 's/ /\\040/g') + printf "\$a\nLABEL=%s /nix apfs rw\n.\nwq\n" "$label" | EDITOR=ed sudo vifs + sudo defaults write /Library/Preferences/SystemConfiguration/autodiskmount AutomountDisksWithoutUserLogin -bool true + fi +} + +main "$@" diff --git a/lib/main.js b/lib/main.js index d5999ad..efc2bf3 100644 --- a/lib/main.js +++ b/lib/main.js @@ -19,7 +19,10 @@ Object.defineProperty(exports, "__esModule", { value: true }); const core = __importStar(require("@actions/core")); const exec = __importStar(require("@actions/exec")); const tc = __importStar(require("@actions/tool-cache")); +const child_process_1 = require("child_process"); const os_1 = require("os"); +const process_1 = require("process"); +const net_1 = require("net"); function nixConf() { return __awaiter(this, void 0, void 0, function* () { // Workaround a segfault: https://github.com/NixOS/nix/issues/2733 @@ -35,22 +38,14 @@ function run() { return __awaiter(this, void 0, void 0, function* () { try { const PATH = process.env.PATH; - const INSTALL_PATH = '/opt/nix'; yield nixConf(); // Catalina workaround https://github.com/NixOS/nix/issues/2925 if (os_1.type() == "Darwin") { - yield exec.exec("sudo", ["sh", "-c", `echo \"nix\t${INSTALL_PATH}\" >> /etc/synthetic.conf`]); - yield exec.exec("sudo", ["sh", "-c", `mkdir -m 0755 ${INSTALL_PATH} && chown runner ${INSTALL_PATH}`]); - yield exec.exec("/System/Library/Filesystems/apfs.fs/Contents/Resources/apfs.util", ["-B"]); - // Needed for sudo to pass NIX_IGNORE_SYMLINK_STORE - yield exec.exec("sudo", ["sh", "-c", "echo 'Defaults env_keep += NIX_IGNORE_SYMLINK_STORE' >> /etc/sudoers"]); - core.exportVariable('NIX_IGNORE_SYMLINK_STORE', "1"); - // Needed for nix-daemon installation - yield exec.exec("sudo", ["launchctl", "setenv", "NIX_IGNORE_SYMLINK_STORE", "1"]); + child_process_1.execFileSync(`${__dirname}/create-darwin-volume.sh`, { stdio: 'inherit' }); } // Needed due to multi-user being too defensive core.exportVariable('ALLOW_PREEXISTING_INSTALLATION', "1"); - // TODO: retry due to all the things that go wrong + // TODO: retry due to all the things that can go wrong const nixInstall = yield tc.downloadTool('https://nixos.org/nix/install'); yield exec.exec("sh", [nixInstall, "--daemon"]); // write nix.conf again as installation overwrites it, reload the daemon to pick up changes @@ -63,9 +58,7 @@ function run() { // macOS needs certificates hints core.exportVariable('NIX_SSL_CERT_FILE', '/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt'); // TODO: nc doesn't work correctly on macOS :( - //await exec.exec("sh", ["-c", "while ! nc -zU /nix/var/nix/daemon-socket/socket; do sleep 0.5; done"]); - // macOS needs time to reload the daemon :( - yield exec.exec("sleep", ["10"]); + yield awaitSocket(); } } catch (error) { @@ -74,4 +67,17 @@ function run() { } }); } +function awaitSocket() { + return __awaiter(this, void 0, void 0, function* () { + const daemonSocket = net_1.createConnection({ path: '/nix/var/nix/daemon-socket/socket' }); + daemonSocket.on('error', () => __awaiter(this, void 0, void 0, function* () { + console.log('Waiting for daemon socket to be available, reconnecting...'); + yield new Promise(resolve => setTimeout(resolve, 500)); + yield awaitSocket(); + })); + daemonSocket.on('connect', () => { + process_1.exit(0); + }); + }); +} run(); diff --git a/src/main.ts b/src/main.ts index d6434db..3ab93fb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,10 @@ import * as core from '@actions/core'; import * as exec from '@actions/exec'; import * as tc from '@actions/tool-cache'; +import {execFileSync} from 'child_process'; import {type} from 'os'; +import {exit} from 'process'; +import {createConnection} from 'net'; async function nixConf() { // Workaround a segfault: https://github.com/NixOS/nix/issues/2733 @@ -17,28 +20,19 @@ async function nixConf() { async function run() { try { - const PATH = process.env.PATH; - const INSTALL_PATH = '/opt/nix'; + const PATH = process.env.PATH; await nixConf(); // Catalina workaround https://github.com/NixOS/nix/issues/2925 if (type() == "Darwin") { - await exec.exec("sudo", ["sh", "-c", `echo \"nix\t${INSTALL_PATH}\" >> /etc/synthetic.conf`]); - await exec.exec("sudo", ["sh", "-c", `mkdir -m 0755 ${INSTALL_PATH} && chown runner ${INSTALL_PATH}`]); - await exec.exec("/System/Library/Filesystems/apfs.fs/Contents/Resources/apfs.util", ["-B"]); - - // Needed for sudo to pass NIX_IGNORE_SYMLINK_STORE - await exec.exec("sudo", ["sh", "-c", "echo 'Defaults env_keep += NIX_IGNORE_SYMLINK_STORE' >> /etc/sudoers"]); - core.exportVariable('NIX_IGNORE_SYMLINK_STORE', "1"); - // Needed for nix-daemon installation - await exec.exec("sudo", ["launchctl", "setenv", "NIX_IGNORE_SYMLINK_STORE", "1"]); + execFileSync(`${__dirname}/create-darwin-volume.sh`, { stdio: 'inherit' }); } // Needed due to multi-user being too defensive core.exportVariable('ALLOW_PREEXISTING_INSTALLATION', "1"); - // TODO: retry due to all the things that go wrong + // TODO: retry due to all the things that can go wrong const nixInstall = await tc.downloadTool('https://nixos.org/nix/install'); await exec.exec("sh", [nixInstall, "--daemon"]); @@ -54,15 +48,24 @@ async function run() { core.exportVariable('NIX_SSL_CERT_FILE', '/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt'); // TODO: nc doesn't work correctly on macOS :( - //await exec.exec("sh", ["-c", "while ! nc -zU /nix/var/nix/daemon-socket/socket; do sleep 0.5; done"]); - // macOS needs time to reload the daemon :( - await exec.exec("sleep", ["10"]); + await awaitSocket(); } - } catch (error) { core.setFailed(`Action failed with error: ${error}`); throw(error); - } + } +} + +async function awaitSocket() { + const daemonSocket = createConnection({ path: '/nix/var/nix/daemon-socket/socket' }); + daemonSocket.on('error', async () => { + console.log('Waiting for daemon socket to be available, reconnecting...'); + await new Promise(resolve => setTimeout(resolve, 500)); + await awaitSocket(); + }); + daemonSocket.on('connect', () => { + exit(0); + }); } run();