diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..c151096 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,24 @@ +name: Tests + +on: + push: + branches: [main, master] + pull_request: + +jobs: + test: + strategy: + matrix: + os: [macos-latest, ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - name: Install Nix + uses: nixbuild/nix-quick-install-action@v34 + + - name: Install just + uses: extractions/setup-just@v2 + + - name: Run tests + run: just test diff --git a/examples/claude-sandboxed/flake.lock b/examples/claude-sandboxed/flake.lock index c738275..20a19bd 100644 --- a/examples/claude-sandboxed/flake.lock +++ b/examples/claude-sandboxed/flake.lock @@ -20,18 +20,14 @@ }, "landrun-nix": { "locked": { - "lastModified": 1762803344, - "narHash": "sha256-HRQe5Umj8+U+euoqlL/Z4DqrKGF/EuglbD0zPNf9dPs=", - "owner": "srid", - "repo": "landrun-nix", - "rev": "e3811659e279d92104a1de82e94877f870065696", - "type": "github" + "path": "../../", + "type": "path" }, "original": { - "owner": "srid", - "repo": "landrun-nix", - "type": "github" - } + "path": "../../", + "type": "path" + }, + "parent": [] }, "nixpkgs": { "locked": { diff --git a/examples/claude-sandboxed/flake.nix b/examples/claude-sandboxed/flake.nix index c675911..4a858d4 100644 --- a/examples/claude-sandboxed/flake.nix +++ b/examples/claude-sandboxed/flake.nix @@ -2,7 +2,7 @@ inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; flake-parts.url = "github:hercules-ci/flake-parts"; - landrun-nix.url = "github:srid/landrun-nix"; + landrun-nix.url = "path:../../"; }; outputs = inputs@{ flake-parts, landrun-nix, ... }: @@ -18,6 +18,7 @@ }; landrunApps.default = { + name = "claude"; imports = [ landrun-nix.landrunModules.gh # So, Claude can run `gh` CLI landrun-nix.landrunModules.git # So, Claude can run `git` CLI diff --git a/justfile b/justfile index ca71719..45611fa 100644 --- a/justfile +++ b/justfile @@ -1,3 +1,15 @@ # Run the claude-sandboxed example with local landrun-nix override run-example: nix run ./examples/claude-sandboxed --override-input landrun-nix . + +# Run integration tests +test: + #!/usr/bin/env bash + if [ "$(uname)" = "Darwin" ]; then + # macOS: BSD script syntax: script -q + SCRIPT_ARGS="-q /dev/null ./tests/test.bats" + else + # Linux: util-linux script syntax: script -qec + SCRIPT_ARGS="-qec ./tests/test.bats /dev/null" + fi + nix develop ./tests --override-input landrun-nix path:./. -c script $SCRIPT_ARGS diff --git a/modules/flake-parts/landrun/common/features.nix b/modules/flake-parts/landrun/common/features.nix new file mode 100644 index 0000000..03486f9 --- /dev/null +++ b/modules/flake-parts/landrun/common/features.nix @@ -0,0 +1,63 @@ +{ lib, config, ... }: +{ + config = { + # Auto-configure CLI options based on high-level flags + cli = lib.mkMerge [ + # TTY support + (lib.mkIf config.features.tty { + rw = [ + "/dev/null" + "/dev/tty" + ]; + rox = [ + "/dev/zero" + "/dev/random" + "/dev/urandom" + "/usr/share/terminfo" + ]; + env = [ + "TERM" + "SHELL" + "COLORTERM" + "LANG" + "LC_ALL" + ]; + }) + + # Nix support + (lib.mkIf config.features.nix { + rox = [ + "/nix" + "/usr" + "/lib" + ]; + rw = [ + "$HOME/.cache/nix" + ]; + ro = [ + "/etc/nix" + "$HOME/.local/share/nix" + ]; + env = [ + "PATH" # Required for programs to find executables + "NIX_PATH" + "NIX_SSL_CERT_FILE" + ]; + }) + + # Network support + (lib.mkIf config.features.network { + rox = [ + "/etc/resolv.conf" + "/etc/ssl" + ]; + unrestrictedNetwork = true; + }) + + # Tmp support + (lib.mkIf config.features.tmp { + rwx = [ "/tmp" ]; + }) + ]; + }; +} diff --git a/modules/flake-parts/landrun/darwin/features.nix b/modules/flake-parts/landrun/darwin/features.nix new file mode 100644 index 0000000..1b6733d --- /dev/null +++ b/modules/flake-parts/landrun/darwin/features.nix @@ -0,0 +1,28 @@ +{ lib, config, pkgs, ... }: +{ + config = lib.mkIf pkgs.stdenv.isDarwin { + cli = lib.mkMerge [ + # TTY support + (lib.mkIf config.features.tty { + rox = [ + "/etc/profile" # Shell initialization + ]; + }) + + # Nix support + (lib.mkIf config.features.nix { + rox = [ + "/bin" + ]; + ro = [ + "/var/run/syslog" # Often needed for logging on macOS + ]; + }) + + # Tmp support + (lib.mkIf config.features.tmp { + rwx = [ "/var/folders" ]; + }) + ]; + }; +} diff --git a/modules/flake-parts/landrun/darwin/wrapper.nix b/modules/flake-parts/landrun/darwin/wrapper.nix new file mode 100644 index 0000000..0e5ab30 --- /dev/null +++ b/modules/flake-parts/landrun/darwin/wrapper.nix @@ -0,0 +1,157 @@ +{ lib, config, pkgs, ... }: +let + pkg = pkgs.writeShellApplication { + name = config.name; + runtimeInputs = [ pkgs.coreutils ]; + text = '' + user=''${USER:-nobody} + PROFILE_FILE=$(mktemp "/tmp/sandbox-profile-$user-XXXXXX.sb") + trap 'rm -f "$PROFILE_FILE"' EXIT + + cat > "$PROFILE_FILE" <> "$PROFILE_FILE" + echo "(allow process-exec (subpath \"$p_real\"))" >> "$PROFILE_FILE" + ;; + ro) + echo "(allow file-read* (subpath \"$p_real\"))" >> "$PROFILE_FILE" + ;; + rw) + echo "(allow file-read* (subpath \"$p_real\"))" >> "$PROFILE_FILE" + echo "(allow file-write* (subpath \"$p_real\"))" >> "$PROFILE_FILE" + echo "(allow file-ioctl (subpath \"$p_real\"))" >> "$PROFILE_FILE" + ;; + rwx) + echo "(allow file-read* (subpath \"$p_real\"))" >> "$PROFILE_FILE" + echo "(allow file-write* (subpath \"$p_real\"))" >> "$PROFILE_FILE" + echo "(allow file-ioctl (subpath \"$p_real\"))" >> "$PROFILE_FILE" + echo "(allow process-exec (subpath \"$p_real\"))" >> "$PROFILE_FILE" + ;; + esac + fi + done + } + + # Fix getcwd by allowing traversal of parents + # Use physical path for CURR_PATH + CURR_PATH="$(pwd -P)" + while [ "$CURR_PATH" != "/" ]; do + echo "(allow file-read* (literal \"$CURR_PATH\"))" >> "$PROFILE_FILE" + CURR_PATH=$(dirname "$CURR_PATH") + done + + ${lib.optionalString config.cli.addExec '' + echo "(allow file-read* (literal \"${config.program}\"))" >> "$PROFILE_FILE" + echo "(allow process-exec (literal \"${config.program}\"))" >> "$PROFILE_FILE" + ''} + + # shellcheck disable=SC2016 + ${lib.optionalString (config.cli.rox != []) "add_paths rox ${lib.escapeShellArgs config.cli.rox}\n"} + # shellcheck disable=SC2016 + ${lib.optionalString (config.cli.ro != []) "add_paths ro ${lib.escapeShellArgs config.cli.ro}\n"} + # shellcheck disable=SC2016 + ${lib.optionalString (config.cli.rw != []) "add_paths rw ${lib.escapeShellArgs config.cli.rw}\n"} + # shellcheck disable=SC2016 + ${lib.optionalString (config.cli.rwx != []) "add_paths rwx ${lib.escapeShellArgs config.cli.rwx}\n"} + + # Execute with isolated environment + exec env -i "''${ENV_ARGS[@]}" sandbox-exec -f "$PROFILE_FILE" ${config.program} "$@" + ''; + }; +in +{ + config = lib.mkIf pkgs.stdenv.isDarwin { + wrappedPackage = + if config.cli.extraArgs != [ ] then + lib.warn "landrun-nix: extraArgs are ignored on Darwin as sandbox-exec does not support them." pkg + else + pkg; + }; +} diff --git a/modules/flake-parts/landrun/default.nix b/modules/flake-parts/landrun/default.nix index c224a26..ee4d68f 100644 --- a/modules/flake-parts/landrun/default.nix +++ b/modules/flake-parts/landrun/default.nix @@ -17,7 +17,7 @@ in apps = lib.mapAttrs (name: cfg: { type = "app"; - program = "${cfg.wrappedPackage}/bin/${name}"; + program = lib.getExe cfg.wrappedPackage; meta = cfg.meta; }) config.landrunApps; diff --git a/modules/flake-parts/landrun/features.nix b/modules/flake-parts/landrun/features.nix index 779bcce..7f71cde 100644 --- a/modules/flake-parts/landrun/features.nix +++ b/modules/flake-parts/landrun/features.nix @@ -1,83 +1,7 @@ -{ lib, config, ... }: { - config = { - # Auto-configure CLI options based on high-level flags - cli = lib.mkMerge [ - # TTY support - (lib.mkIf config.features.tty { - rw = [ - "/dev/null" - "/dev/tty" - "/dev/pts" - "/dev/ptmx" - ]; - rox = [ - "/dev/zero" - "/dev/full" - "/dev/random" - "/dev/urandom" - "/etc/terminfo" - "/etc/profile" # Shell initialization - "/usr/share/terminfo" - ]; - env = [ - "TERM" - "SHELL" - "COLORTERM" - "LANG" - "LC_ALL" - ]; - }) - - # Nix support - (lib.mkIf config.features.nix { - rox = [ - "/nix" - "/usr" - "/lib" - "/lib64" - ]; - rw = [ - "$HOME/.cache/nix" - ]; - ro = [ - "/proc/self" # Required for GC to read thread stack info - "/proc/stat" - "/etc/nix" - "$HOME/.local/share/nix" - ]; - env = [ - "PATH" # Required for programs to find executables - "NIX_PATH" - "NIX_SSL_CERT_FILE" - ]; - }) - - # Network support - (lib.mkIf config.features.network { - rox = [ - "/etc/resolv.conf" - "/etc/ssl" - ]; - unrestrictedNetwork = true; - }) - - # Tmp support - (lib.mkIf config.features.tmp { - rw = [ "/tmp" ]; - }) - - # D-Bus support (for keyring/Secret Service API) - (lib.mkIf config.features.dbus { - rw = [ - "$HOME/.local/share/keyrings" # Keyring storage - "/run/user/$UID/bus" # D-Bus socket - ]; - env = [ - "DBUS_SESSION_BUS_ADDRESS" # D-Bus session bus - "XDG_RUNTIME_DIR" # Runtime directory - ]; - }) - ]; - }; + imports = [ + ./common/features.nix + ./darwin/features.nix + ./linux/features.nix + ]; } diff --git a/modules/flake-parts/landrun/linux/features.nix b/modules/flake-parts/landrun/linux/features.nix new file mode 100644 index 0000000..9c88aa1 --- /dev/null +++ b/modules/flake-parts/landrun/linux/features.nix @@ -0,0 +1,43 @@ +{ lib, config, pkgs, ... }: +{ + config = lib.mkIf (!pkgs.stdenv.isDarwin) { + # Auto-configure CLI options based on high-level flags + cli = lib.mkMerge [ + # TTY support + (lib.mkIf config.features.tty { + rw = [ + "/dev/pts" + "/dev/ptmx" + ]; + rox = [ + "/dev/full" + "/etc/terminfo" + "/etc/profile" # Shell initialization + ]; + }) + + # Nix support + (lib.mkIf config.features.nix { + rox = [ + "/lib64" + ]; + ro = [ + "/proc/self" # Required for GC to read thread stack info + "/proc/stat" + ]; + }) + + # D-Bus support (for keyring/Secret Service API) + (lib.mkIf config.features.dbus { + rw = [ + "/run/user/$UID/bus" # D-Bus socket + "$HOME/.local/share/keyrings" # Keyring storage + ]; + env = [ + "DBUS_SESSION_BUS_ADDRESS" # D-Bus session bus + "XDG_RUNTIME_DIR" # Runtime directory + ]; + }) + ]; + }; +} diff --git a/modules/flake-parts/landrun/linux/wrapper.nix b/modules/flake-parts/landrun/linux/wrapper.nix new file mode 100644 index 0000000..32436da --- /dev/null +++ b/modules/flake-parts/landrun/linux/wrapper.nix @@ -0,0 +1,49 @@ +{ lib, config, pkgs, ... }: +{ + config = lib.mkIf (!pkgs.stdenv.isDarwin) { + wrappedPackage = + let + # Helper to generate conditional path argument + conditionalPathArg = flag: paths: + lib.concatMapStringsSep "\n" + (p: '' + if [ -e "${p}" ]; then + args+=("${flag}" "${p}") + fi + '') + paths; + + # Static args (non-path related) + staticArgs = lib.concatStringsSep " \\\n " + ([ ] + ++ (map (p: "--rwx \"${p}\"") config.cli.rwx) + ++ (map (p: "--rw \"${p}\"") config.cli.rw) + ++ (map (e: "--env ${e}") config.cli.env) + ++ (lib.optional config.cli.unrestrictedNetwork "--unrestricted-network") + ++ (lib.optional config.cli.unrestrictedFilesystem "--unrestricted-filesystem") + ++ (lib.optional config.cli.addExec "--add-exec") + ++ config.cli.extraArgs + ); + in + (pkgs.writeShellApplication { + name = config.name; + runtimeInputs = [ pkgs.landrun ]; + text = '' + args=() + + # Add conditional --rox paths + ${conditionalPathArg "--rox" config.cli.rox} + + # Add conditional --ro paths + ${conditionalPathArg "--ro" config.cli.ro} + + exec landrun \ + "''${args[@]}" \ + ${staticArgs} \ + ${config.program} "$@" + ''; + }) // { + meta = config.meta; + }; + }; +} diff --git a/modules/flake-parts/landrun/options.nix b/modules/flake-parts/landrun/options.nix index a719420..b3dc309 100644 --- a/modules/flake-parts/landrun/options.nix +++ b/modules/flake-parts/landrun/options.nix @@ -1,4 +1,4 @@ -{ lib, ... }: +{ lib, name ? null, ... }: let inherit (lib) mkOption @@ -6,11 +6,17 @@ let in { options = { + name = mkOption { + type = types.str; + description = "Name given to the wrapper program. Defaults to submodule.name if used in attrsOf submodule."; + }; + program = mkOption { type = types.str; description = "The program to wrap with landrun (e.g., \${pkgs.foo}/bin/foo)"; }; + features = mkOption { type = types.submodule { options = { @@ -35,7 +41,7 @@ in tmp = mkOption { type = types.bool; default = true; - description = "Enable read-write access to /tmp for temporary files"; + description = "Enable read-write-execute access to /tmp for temporary files"; }; dbus = mkOption { @@ -123,4 +129,6 @@ in description = "The resulting wrapped package (internal)"; }; }; + + config.name = lib.mkIf (name != null) (lib.mkDefault name); } diff --git a/modules/flake-parts/landrun/wrapper.nix b/modules/flake-parts/landrun/wrapper.nix index 4733e5a..73f6a96 100644 --- a/modules/flake-parts/landrun/wrapper.nix +++ b/modules/flake-parts/landrun/wrapper.nix @@ -1,49 +1,6 @@ -{ lib, config, pkgs, name, ... }: { - config = { - wrappedPackage = - let - # Helper to generate conditional path argument - conditionalPathArg = flag: paths: - lib.concatMapStringsSep "\n" - (p: '' - if [ -e "${p}" ]; then - args+=("${flag}" "${p}") - fi - '') - paths; - - # Static args (non-path related) - staticArgs = lib.concatStringsSep " \\\n " - ([ ] - ++ (map (p: "--rwx \"${p}\"") config.cli.rwx) - ++ (map (p: "--rw \"${p}\"") config.cli.rw) - ++ (map (e: "--env ${e}") config.cli.env) - ++ (lib.optional config.cli.unrestrictedNetwork "--unrestricted-network") - ++ (lib.optional config.cli.unrestrictedFilesystem "--unrestricted-filesystem") - ++ (lib.optional config.cli.addExec "--add-exec") - ++ config.cli.extraArgs - ); - in - (pkgs.writeShellApplication { - name = name; - runtimeInputs = [ pkgs.landrun ]; - text = '' - args=() - - # Add conditional --rox paths - ${conditionalPathArg "--rox" config.cli.rox} - - # Add conditional --ro paths - ${conditionalPathArg "--ro" config.cli.ro} - - exec landrun \ - "''${args[@]}" \ - ${staticArgs} \ - ${config.program} "$@" - ''; - }) // { - meta = config.meta; - }; - }; + imports = [ + ./darwin/wrapper.nix + ./linux/wrapper.nix + ]; } diff --git a/tests/flake.nix b/tests/flake.nix index 5b377b5..1a0eb38 100644 --- a/tests/flake.nix +++ b/tests/flake.nix @@ -7,13 +7,14 @@ outputs = inputs@{ flake-parts, ... }: flake-parts.lib.mkFlake { inherit inputs; } { - debug = true; systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; imports = [ inputs.landrun-nix.flakeModule ]; perSystem = { config, pkgs, ... }: let - testDeps = [ pkgs.bats ] ++ (builtins.attrValues config.packages); + testDeps = [ pkgs.bats ] + ++ (builtins.attrValues config.packages) + ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.util-linux ]; in { landrunApps = { @@ -24,6 +25,18 @@ program = "${pkgs.coreutils}/bin/ls"; features.tty = false; }; + test-tty = { + program = "${pkgs.coreutils}/bin/stty"; + features.tty = true; + }; + test-mktemp = { + program = "${pkgs.coreutils}/bin/mktemp"; + features.tmp = true; + }; + test-mktemp-no-tmp = { + program = "${pkgs.coreutils}/bin/mktemp"; + features.tmp = false; + }; test-curl-deny = { program = "${pkgs.curl}/bin/curl"; features.network = false; @@ -121,18 +134,28 @@ checks.tests = pkgs.runCommand "tests" { __impure = true; - nativeBuildInputs = [ pkgs.bats pkgs.glibc ] ++ testDeps; + nativeBuildInputs = + [ pkgs.bats ] + ++ testDeps + ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.util-linux pkgs.glibc ] + ; } '' export HOME=$(realpath ./home) mkdir -p $HOME mkdir -p $HOME/.cache/nix - + mkdir -p /etc export NIX_SSL_CERT_FILE="${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" # Used to skip some tests which fail in nix sandbox on CI export IN_NIX_SANBOX=1 - bats ${./test.bats} | tee $out + + # Wrap bats in script to fake a TTY, required for test-tty + ${if pkgs.stdenv.isLinux then '' + script -qec "bats ${./test.bats}" /dev/null | tee $out + '' else '' + bats ${./test.bats} | tee $out + ''} ''; }; }; diff --git a/tests/test.bats b/tests/test.bats index df86b4d..3558645 100755 --- a/tests/test.bats +++ b/tests/test.bats @@ -7,6 +7,7 @@ setup() { TEST_TEMP_DIR="$(mktemp -d)" cd "$TEST_TEMP_DIR" echo "This is a secret" > test_secret + OS="$(uname -s)" } teardown() { # shellcheck disable=SC2164 @@ -28,10 +29,13 @@ log_output () { @test "test-no-nix-fail: program cannot exec if it cannot access libs from nix store" { run test-no-nix-fail -c "echo ok" log_output - [ "$status" -eq 1 ] + [ "$status" -ne 0 ] } @test "test-no-nix-ldd-ok: program can exec if libs are made accessible with --ldd flag" { + if [ "$OS" == "Darwin" ]; then + skip "landrun specific flag --ldd is not supported on Darwin" + fi run test-no-nix-ldd-ok -c "echo ok" log_output [ "$status" -eq 0 ] @@ -41,10 +45,13 @@ log_output () { @test "test-add-exec-disabled-fail: program cannot exec if not explicitly allowed" { run test-add-exec-disabled-fail -c "echo ok" log_output - [ "$status" -eq 1 ] + [ "$status" -ne 0 ] } @test "test-add-exec-disabled-ldd-ok: script can exec if not explicitly allowed but interpreter and libs are" { + if [ "$OS" == "Darwin" ]; then + skip "landrun specific flag --ldd is not supported on Darwin" + fi run test-add-exec-disabled-ldd-ok -c "echo ok" log_output [ "$status" -eq 0 ] @@ -53,6 +60,9 @@ log_output () { @test "test-extra-args: passes extra arguments to landrun" { + if [ "$OS" == "Darwin" ]; then + skip "landrun specific flag -v (version) is not supported on Darwin" + fi # We configured test-extra-args with cli.extraArgs = [ "-v" ] # In landrun, -v flag prints the version and exits. run test-extra-args -c "echo ok" @@ -61,12 +71,60 @@ log_output () { [[ "$output" == *"landrun version"* ]] } + + @test "test-ls can list /tmp" { run test-ls /tmp log_output [ "$status" -eq 0 ] } +@test "test-mktemp can write to /tmp" { + run test-mktemp /tmp/test.XXXXXX + log_output + [ "$status" -eq 0 ] + # Check if output looks like a path + [[ "$output" == /tmp/test.* ]] +} + +@test "test-mktemp can write to default tmp directory" { + run test-mktemp + log_output + [ "$status" -eq 0 ] + # Verify the file was actually created and is writable + [ -f "$output" ] + [ -w "$output" ] + rm "$output" +} + +@test "test-mktemp-no-tmp fails to write to /tmp" { + run test-mktemp-no-tmp /tmp/test.XXXXXX + log_output + [ "$status" -ne 0 ] +} + +@test "test-exec-tmp can execute script in /tmp" { + # We use test-env-var (bash) as it has features.tmp = true (default) + run test-env-var -c ' + SCRIPT=$(mktemp /tmp/test-script.XXXXXX) + echo "#!$BASH" > "$SCRIPT" + echo "echo executed" >> "$SCRIPT" + chmod +x "$SCRIPT" + "$SCRIPT" + ' + log_output + # This implies checking if execution is allowed. + # If status is 0, it allowed execution. + # If status is 126 or 1 (EPERM), it denied. + # We expect failure currently if tmp is not rwx + if [ "$status" -eq 0 ]; then + echo "Execution allowed" + else + echo "Execution denied" + false + fi +} + @test "test-ls can list /nix/store" { run test-ls -d /nix/store log_output @@ -74,11 +132,24 @@ log_output () { } @test "test-ls cannot list / (restricted by default)" { - run test-ls / + run test-ls /etc log_output [ "$status" -ne 0 ] } +@test "test-tty can access terminal info" { + # This tries to read terminal settings + run test-tty -a + log_output + [ "$status" -eq 0 ] + + # This tries to set terminal settings (requires write/ioctl access) + # using 'stty sane' which resets terminal to sane values + run test-tty sane + log_output + [ "$status" -eq 0 ] +} + @test "test-curl-deny fails to connect to google.com" { run test-curl-deny --connect-timeout 2 https://google.com log_output @@ -127,7 +198,8 @@ log_output () { run test-no-access -c "cat test_secret" log_output [ "$status" -ne 0 ] - [ "$output" == "cat: test_secret: Permission denied" ] + # Linux (landrun) returns "Permission denied", Darwin (sandbox-exec) returns "Operation not permitted" + [[ "$output" == "cat: test_secret: Permission denied" || "$output" == "cat: test_secret: Operation not permitted" ]] } @test "test-multi-paths: respects multiple paths" { @@ -199,17 +271,28 @@ log_output () { } @test "test-special-env: passes special characters and multiline" { - export SPECIAL_VAR="line1 + export NOT_INHERITED='abc + --efd + ' + export SPECIAL_VAR='line1 line2 -special !@#\$%^&*()" + special !@#\$%^&*()' + run test-special-env -c "echo \"\$SPECIAL_VAR\"" log_output + [ "$status" -eq 0 ] [ "$output" == "$SPECIAL_VAR" ] + + local expected_line_count=3 + if [ "$(wc -l <<< "$output")" -ne $expected_line_count ]; then + echo "Error: output must contain exactly $expected_line_count lines." + return 1 + fi } @test "test-unrestricted-fs: can access /" { - run test-unrestricted-fs -c "ls -d /" + run test-unrestricted-fs -c "ls -d /etc" log_output [ "$status" -eq 0 ] } diff --git a/vira.hs b/vira.hs index f7d6532..df967f4 100644 --- a/vira.hs +++ b/vira.hs @@ -7,6 +7,7 @@ pipeline { build.systems = [ "x86_64-linux" + , "aarch64-darwin" ] , build.flakes = [ "."