diff --git a/.github/workflows/ci.generated.yml b/.github/workflows/ci.generated.yml index dae6cdd22d608b..b81e8ba982ba0d 100644 --- a/.github/workflows/ci.generated.yml +++ b/.github/workflows/ci.generated.yml @@ -614,6 +614,20 @@ jobs: [ -f "$c" ] && DENO_BIN="$c" && break done "$DENO_BIN" run -A ./tools/download_tsc.ts + - name: Set up native laufey cache + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 + if: '!startsWith(github.ref, ''refs/tags/'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + with: + path: ./target/.native_laufey + key: 'laufey-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles(''cli/laufey_sums.lock'') }}' + - name: Pre-download native laufey + if: '!startsWith(github.ref, ''refs/tags/'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + run: |- + DENO_BIN="" + for c in ./target/release/deno ./target/release/deno.exe ./target/debug/deno ./target/debug/deno.exe; do + [ -f "$c" ] && DENO_BIN="$c" && break + done + "$DENO_BIN" run -A ./tools/download_laufey.ts - name: Build ffi (debug) if: '!startsWith(github.ref, ''refs/tags/'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' run: cargo build -p test_ffi @@ -1172,6 +1186,20 @@ jobs: [ -f "$c" ] && DENO_BIN="$c" && break done "$DENO_BIN" run -A ./tools/download_tsc.ts + - name: Set up native laufey cache + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 + if: '!startsWith(github.ref, ''refs/tags/'') && !(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + with: + path: ./target/.native_laufey + key: 'laufey-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles(''cli/laufey_sums.lock'') }}' + - name: Pre-download native laufey + if: '!startsWith(github.ref, ''refs/tags/'') && !(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + run: |- + DENO_BIN="" + for c in ./target/release/deno ./target/release/deno.exe ./target/debug/deno ./target/debug/deno.exe; do + [ -f "$c" ] && DENO_BIN="$c" && break + done + "$DENO_BIN" run -A ./tools/download_laufey.ts - name: Build ffi (release) if: '!startsWith(github.ref, ''refs/tags/'') && !(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' run: cargo build --release -p test_ffi @@ -1565,6 +1593,20 @@ jobs: [ -f "$c" ] && DENO_BIN="$c" && break done "$DENO_BIN" run -A ./tools/download_tsc.ts + - name: Set up native laufey cache + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 + if: '!startsWith(github.ref, ''refs/tags/'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + with: + path: ./target/.native_laufey + key: 'laufey-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles(''cli/laufey_sums.lock'') }}' + - name: Pre-download native laufey + if: '!startsWith(github.ref, ''refs/tags/'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + run: |- + DENO_BIN="" + for c in ./target/release/deno ./target/release/deno.exe ./target/debug/deno ./target/debug/deno.exe; do + [ -f "$c" ] && DENO_BIN="$c" && break + done + "$DENO_BIN" run -A ./tools/download_laufey.ts - name: Build ffi (debug) if: '!startsWith(github.ref, ''refs/tags/'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' run: cargo build -p test_ffi @@ -2323,6 +2365,20 @@ jobs: [ -f "$c" ] && DENO_BIN="$c" && break done "$DENO_BIN" run -A ./tools/download_tsc.ts + - name: Set up native laufey cache + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 + if: '!startsWith(github.ref, ''refs/tags/'') && !(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + with: + path: ./target/.native_laufey + key: 'laufey-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles(''cli/laufey_sums.lock'') }}' + - name: Pre-download native laufey + if: '!startsWith(github.ref, ''refs/tags/'') && !(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + run: |- + DENO_BIN="" + for c in ./target/release/deno ./target/release/deno.exe ./target/debug/deno ./target/debug/deno.exe; do + [ -f "$c" ] && DENO_BIN="$c" && break + done + "$DENO_BIN" run -A ./tools/download_laufey.ts - name: Build ffi (release) if: '!startsWith(github.ref, ''refs/tags/'') && !(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' run: cargo build --release -p test_ffi @@ -2672,6 +2728,20 @@ jobs: [ -f "$c" ] && DENO_BIN="$c" && break done "$DENO_BIN" run -A ./tools/download_tsc.ts + - name: Set up native laufey cache + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 + if: '!startsWith(github.ref, ''refs/tags/'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + with: + path: ./target/.native_laufey + key: 'laufey-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles(''cli/laufey_sums.lock'') }}' + - name: Pre-download native laufey + if: '!startsWith(github.ref, ''refs/tags/'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + run: |- + DENO_BIN="" + for c in ./target/release/deno ./target/release/deno.exe ./target/debug/deno ./target/debug/deno.exe; do + [ -f "$c" ] && DENO_BIN="$c" && break + done + "$DENO_BIN" run -A ./tools/download_laufey.ts - name: Build ffi (debug) if: '!startsWith(github.ref, ''refs/tags/'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' run: cargo build -p test_ffi @@ -3350,6 +3420,20 @@ jobs: [ -f "$c" ] && DENO_BIN="$c" && break done "$DENO_BIN" run -A ./tools/download_tsc.ts + - name: Set up native laufey cache + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 + if: '!startsWith(github.ref, ''refs/tags/'') && !(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + with: + path: ./target/.native_laufey + key: 'laufey-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles(''cli/laufey_sums.lock'') }}' + - name: Pre-download native laufey + if: '!startsWith(github.ref, ''refs/tags/'') && !(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + run: |- + DENO_BIN="" + for c in ./target/release/deno ./target/release/deno.exe ./target/debug/deno ./target/debug/deno.exe; do + [ -f "$c" ] && DENO_BIN="$c" && break + done + "$DENO_BIN" run -A ./tools/download_laufey.ts - name: Build ffi (release) if: '!startsWith(github.ref, ''refs/tags/'') && !(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' run: cargo build --release -p test_ffi @@ -3699,6 +3783,20 @@ jobs: [ -f "$c" ] && DENO_BIN="$c" && break done "$DENO_BIN" run -A ./tools/download_tsc.ts + - name: Set up native laufey cache + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 + if: '!startsWith(github.ref, ''refs/tags/'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + with: + path: ./target/.native_laufey + key: 'laufey-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles(''cli/laufey_sums.lock'') }}' + - name: Pre-download native laufey + if: '!startsWith(github.ref, ''refs/tags/'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + run: |- + DENO_BIN="" + for c in ./target/release/deno ./target/release/deno.exe ./target/debug/deno ./target/debug/deno.exe; do + [ -f "$c" ] && DENO_BIN="$c" && break + done + "$DENO_BIN" run -A ./tools/download_laufey.ts - name: Build ffi (debug) if: '!startsWith(github.ref, ''refs/tags/'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' run: cargo build -p test_ffi @@ -4258,6 +4356,20 @@ jobs: [ -f "$c" ] && DENO_BIN="$c" && break done "$DENO_BIN" run -A ./tools/download_tsc.ts + - name: Set up native laufey cache + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 + if: '!startsWith(github.ref, ''refs/tags/'') && !(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + with: + path: ./target/.native_laufey + key: 'laufey-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles(''cli/laufey_sums.lock'') }}' + - name: Pre-download native laufey + if: '!startsWith(github.ref, ''refs/tags/'') && !(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + run: |- + DENO_BIN="" + for c in ./target/release/deno ./target/release/deno.exe ./target/debug/deno ./target/debug/deno.exe; do + [ -f "$c" ] && DENO_BIN="$c" && break + done + "$DENO_BIN" run -A ./tools/download_laufey.ts - name: Build ffi (release) if: '!startsWith(github.ref, ''refs/tags/'') && !(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' run: cargo build --release -p test_ffi @@ -5044,6 +5156,20 @@ jobs: [ -f "$c" ] && DENO_BIN="$c" && break done "$DENO_BIN" run -A ./tools/download_tsc.ts + - name: Set up native laufey cache + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 + if: '!startsWith(github.ref, ''refs/tags/'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + with: + path: ./target/.native_laufey + key: 'laufey-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles(''cli/laufey_sums.lock'') }}' + - name: Pre-download native laufey + if: '!startsWith(github.ref, ''refs/tags/'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + run: |- + DENO_BIN="" + for c in ./target/release/deno ./target/release/deno.exe ./target/debug/deno ./target/debug/deno.exe; do + [ -f "$c" ] && DENO_BIN="$c" && break + done + "$DENO_BIN" run -A ./tools/download_laufey.ts - name: Build ffi (release) if: '!startsWith(github.ref, ''refs/tags/'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' run: cargo build --release -p test_ffi @@ -5052,7 +5178,7 @@ jobs: env: CI_SHARD_INDEX: '${{ github.event_name == ''pull_request'' && matrix.shard_index || '''' }}' CI_SHARD_TOTAL: '${{ github.event_name == ''pull_request'' && matrix.shard_total || '''' }}' - run: 'cargo test -p ${{ matrix.test_package }} --test ${{ matrix.test_crate }} --release' + run: 'xvfb-run -a cargo test -p ${{ matrix.test_package }} --test ${{ matrix.test_crate }} --release' - name: Ensure no git changes if: '!startsWith(github.ref, ''refs/tags/'') && github.event_name == ''pull_request''' run: |- @@ -5729,6 +5855,20 @@ jobs: [ -f "$c" ] && DENO_BIN="$c" && break done "$DENO_BIN" run -A ./tools/download_tsc.ts + - name: Set up native laufey cache + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 + if: '!startsWith(github.ref, ''refs/tags/'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + with: + path: ./target/.native_laufey + key: 'laufey-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles(''cli/laufey_sums.lock'') }}' + - name: Pre-download native laufey + if: '!startsWith(github.ref, ''refs/tags/'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + run: |- + DENO_BIN="" + for c in ./target/release/deno ./target/release/deno.exe ./target/debug/deno ./target/debug/deno.exe; do + [ -f "$c" ] && DENO_BIN="$c" && break + done + "$DENO_BIN" run -A ./tools/download_laufey.ts - name: Build ffi (debug) if: '!startsWith(github.ref, ''refs/tags/'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' run: cargo build -p test_ffi @@ -5738,7 +5878,7 @@ jobs: CARGO_PROFILE_DEV_DEBUG: 0 CI_SHARD_INDEX: '${{ github.event_name == ''pull_request'' && matrix.shard_index || '''' }}' CI_SHARD_TOTAL: '${{ github.event_name == ''pull_request'' && matrix.shard_total || '''' }}' - run: 'cargo test -p ${{ matrix.test_package }} --test ${{ matrix.test_crate }}' + run: 'xvfb-run -a cargo test -p ${{ matrix.test_package }} --test ${{ matrix.test_crate }}' - name: Ensure no git changes if: '!startsWith(github.ref, ''refs/tags/'') && github.event_name == ''pull_request''' run: |- @@ -6227,6 +6367,20 @@ jobs: [ -f "$c" ] && DENO_BIN="$c" && break done "$DENO_BIN" run -A ./tools/download_tsc.ts + - name: Set up native laufey cache + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 + if: '!startsWith(github.ref, ''refs/tags/'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + with: + path: ./target/.native_laufey + key: 'laufey-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles(''cli/laufey_sums.lock'') }}' + - name: Pre-download native laufey + if: '!startsWith(github.ref, ''refs/tags/'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + run: |- + DENO_BIN="" + for c in ./target/release/deno ./target/release/deno.exe ./target/debug/deno ./target/debug/deno.exe; do + [ -f "$c" ] && DENO_BIN="$c" && break + done + "$DENO_BIN" run -A ./tools/download_laufey.ts - name: Load 'vsock_loopback; kernel module if: '!startsWith(github.ref, ''refs/tags/'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'')' run: sudo modprobe vsock_loopback @@ -6239,7 +6393,7 @@ jobs: CARGO_PROFILE_DEV_DEBUG: 0 CI_SHARD_INDEX: '${{ github.event_name == ''pull_request'' && matrix.shard_index || '''' }}' CI_SHARD_TOTAL: '${{ github.event_name == ''pull_request'' && matrix.shard_total || '''' }}' - run: 'cargo test -p ${{ matrix.test_package }} --test ${{ matrix.test_crate }}' + run: 'xvfb-run -a cargo test -p ${{ matrix.test_package }} --test ${{ matrix.test_crate }}' - name: Ensure no git changes if: '!startsWith(github.ref, ''refs/tags/'') && github.event_name == ''pull_request''' run: |- @@ -7155,6 +7309,20 @@ jobs: [ -f "$c" ] && DENO_BIN="$c" && break done "$DENO_BIN" run -A ./tools/download_tsc.ts + - name: Set up native laufey cache + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 + if: '!startsWith(github.ref, ''refs/tags/'') && !(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + with: + path: ./target/.native_laufey + key: 'laufey-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles(''cli/laufey_sums.lock'') }}' + - name: Pre-download native laufey + if: '!startsWith(github.ref, ''refs/tags/'') && !(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'') && matrix.test_crate == ''specs''' + run: |- + DENO_BIN="" + for c in ./target/release/deno ./target/release/deno.exe ./target/debug/deno ./target/debug/deno.exe; do + [ -f "$c" ] && DENO_BIN="$c" && break + done + "$DENO_BIN" run -A ./tools/download_laufey.ts - name: Load 'vsock_loopback; kernel module if: '!startsWith(github.ref, ''refs/tags/'') && !(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && (matrix.shard_index == 0 || github.event_name == ''pull_request'')' run: sudo modprobe vsock_loopback @@ -7166,7 +7334,7 @@ jobs: env: CI_SHARD_INDEX: '${{ github.event_name == ''pull_request'' && matrix.shard_index || '''' }}' CI_SHARD_TOTAL: '${{ github.event_name == ''pull_request'' && matrix.shard_total || '''' }}' - run: 'cargo test -p ${{ matrix.test_package }} --test ${{ matrix.test_crate }} --release' + run: 'xvfb-run -a cargo test -p ${{ matrix.test_package }} --test ${{ matrix.test_crate }} --release' - name: Ensure no git changes if: '!startsWith(github.ref, ''refs/tags/'') && !(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && github.event_name == ''pull_request''' run: |- diff --git a/.github/workflows/ci.ts b/.github/workflows/ci.ts index a2c1e4cb9bd4ad..078273f56cdf5a 100755 --- a/.github/workflows/ci.ts +++ b/.github/workflows/ci.ts @@ -1388,6 +1388,12 @@ const buildJobs = buildItems.map((rawBuildItem) => { }), }); const testCrateNameExpr = testMatrix.test_crate; + // Desktop spec tests (part of the "specs" crate) launch a real GUI + // backend, which needs a display. Linux CI runners have no display, so + // run the test binary under a virtual X display via xvfb-run. + const cargoTestCmdPrefix = rawBuildItem.os === "linux" + ? "xvfb-run -a " + : ""; const { restoreCacheStep, saveCacheStep, @@ -1484,6 +1490,36 @@ const buildJobs = buildItems.map((rawBuildItem) => { '"$DENO_BIN" run -A ./tools/download_tsc.ts', ].join("\n"), }, + { + name: "Set up native laufey cache", + if: testCrateNameExpr.equals("specs"), + uses: "actions/cache@v5", + with: { + // Keyed on laufey_sums.lock so a pinned-version bump re-downloads. + path: "./target/.native_laufey", + key: + "laufey-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('cli/laufey_sums.lock') }}", + }, + }, + { + // Warm the cache with the "laufey" desktop backend that `deno + // desktop` downloads (into the default target/.native_laufey) so + // the test step doesn't re-download the (100+ MB) archive for + // every test's fresh DENO_DIR. The harness resolves that path and + // injects DENO_LAUFEY_CACHE_DIR per-test itself (see + // test_util::native_laufey_cache_dir); no env export needed. Run + // it with the built deno binary (the test job has no system + // `deno` on PATH). + name: "Pre-download native laufey", + if: testCrateNameExpr.equals("specs"), + run: [ + 'DENO_BIN=""', + "for c in ./target/release/deno ./target/release/deno.exe ./target/debug/deno ./target/debug/deno.exe; do", + ' [ -f "$c" ] && DENO_BIN="$c" && break', + "done", + '"$DENO_BIN" run -A ./tools/download_laufey.ts', + ].join("\n"), + }, { if: buildItem.os.equals("linux").and( buildItem.arch.equals("aarch64"), @@ -1504,8 +1540,10 @@ const buildJobs = buildItems.map((rawBuildItem) => { { name: "Test (debug)", if: isDebug, + // Desktop spec tests launch a GUI backend, so run under a virtual + // X display on Linux where no real display is available. run: - `cargo test -p ${testMatrix.test_package} --test ${testMatrix.test_crate}`, + `${cargoTestCmdPrefix}cargo test -p ${testMatrix.test_package} --test ${testMatrix.test_crate}`, env: { CARGO_PROFILE_DEV_DEBUG: 0, CI_SHARD_INDEX: isPr.then(testMatrix.shard_index).else(""), @@ -1518,7 +1556,7 @@ const buildJobs = buildItems.map((rawBuildItem) => { isDenoland.or(buildItem.use_sysroot), ), run: - `cargo test -p ${testMatrix.test_package} --test ${testMatrix.test_crate} --release`, + `${cargoTestCmdPrefix}cargo test -p ${testMatrix.test_package} --test ${testMatrix.test_crate} --release`, env: { CI_SHARD_INDEX: isPr.then(testMatrix.shard_index).else(""), CI_SHARD_TOTAL: isPr.then(testMatrix.shard_total).else(""), diff --git a/Cargo.lock b/Cargo.lock index 714be28fe32507..0272f9601d25a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2026,6 +2026,7 @@ dependencies = [ "serde_json", "serde_repr", "sha2", + "shell-words", "shlex", "spki 0.7.3", "strsim", @@ -9385,6 +9386,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + [[package]] name = "shlex" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index d38faa5a5c177f..e2f03ef2f458eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -309,6 +309,7 @@ serde = { version = "1.0.149", features = ["derive"] } serde_bytes = "0.11" serde_json = { version = "1.0.85", features = ["raw_value"] } serde_repr = "=0.1.19" +shell-words = "1.1.0" shlex = "1.3.0" signal-hook = "0.3" slab = "0.4" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index a1d04ada3bbd31..75d2ce542b2e79 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -178,6 +178,7 @@ same-file.workspace = true serde.workspace = true serde_repr.workspace = true sha2.workspace = true +shell-words.workspace = true shlex.workspace = true spki = { workspace = true, features = ["pem"] } strsim.workspace = true diff --git a/cli/schemas/config-file.v1.json b/cli/schemas/config-file.v1.json index 26375ebc779014..8150bd5a61a4f1 100644 --- a/cli/schemas/config-file.v1.json +++ b/cli/schemas/config-file.v1.json @@ -193,6 +193,21 @@ "description": "Backend to use for the desktop app.", "enum": ["webview", "cef"] }, + "backendArgs": { + "type": "object", + "description": "Optional backend-specific arguments.", + "additionalProperties": false, + "properties": { + "cef": { + "type": "string", + "description": "Optional argument for the CEF backend." + }, + "webview": { + "type": "string", + "description": "Optional argument for the webview backend." + } + } + }, "output": { "type": "object", "description": "Platform-specific output paths.", diff --git a/cli/tools/desktop.rs b/cli/tools/desktop.rs index fc172e1d4fd430..3f580853d0df90 100644 --- a/cli/tools/desktop.rs +++ b/cli/tools/desktop.rs @@ -165,6 +165,18 @@ fn apply_desktop_config_to_flags( desktop_flags.backend = Some(backend); } + if let Some(backend_args_config) = desktop_config.backend_args + && desktop_flags.backend_args.is_none() + { + let effective_backend = + desktop_flags.backend.as_deref().unwrap_or("webview"); + desktop_flags.backend_args = match effective_backend { + "cef" => backend_args_config.cef, + "webview" => backend_args_config.webview, + _ => None, + }; + } + if let Some(macos_config) = desktop_config.macos && let Some(identity) = macos_config.codesign_identity && desktop_flags.codesign_identity.is_none() @@ -589,6 +601,7 @@ async fn compile_desktop( &bundle_path, &appimage_abs, desktop_flags.target.as_deref(), + &desktop_flags, )?; appimage_abs } else if let Some(deb) = deb_output.as_deref() { @@ -670,8 +683,10 @@ fn make_self_extracting( }; match target_os { "macos" => make_self_extracting_macos(bundle_path, format, desktop_flags), - "windows" => make_self_extracting_dir(bundle_path, format, true), - _ => make_self_extracting_dir(bundle_path, format, false), + "windows" => { + make_self_extracting_dir(bundle_path, format, true, desktop_flags) + } + _ => make_self_extracting_dir(bundle_path, format, false, desktop_flags), } } @@ -999,6 +1014,12 @@ fn make_self_extracting_macos( let (raw, comp) = write_tar_compressed(staging.path(), &inner_name, &payload, format)?; let hash = payload_hash(&payload)?; + let backend_args = format_backend_args_for_shell(desktop_flags)?; + let launcher_args = if backend_args.is_empty() { + String::new() + } else { + format!(" {backend_args}") + }; let launcher = format!( "#!/bin/sh\n\ @@ -1010,7 +1031,7 @@ fn make_self_extracting_macos( \u{20} mkdir -p \"$DEST\"\n\ \u{20} tar -xf \"$DIR/../Resources/{payload_name}\" -C \"$DEST\"\n\ fi\n\ - exec \"$APP/Contents/MacOS/{app_name}\" \"$@\"\n", + exec \"$APP/Contents/MacOS/{app_name}\"{launcher_args} \"$@\"\n", ); let launcher_path = macos_dir.join(&app_name); std::fs::write(&launcher_path, launcher)?; @@ -1059,6 +1080,7 @@ fn make_self_extracting_dir( bundle_path: &Path, format: &str, windows: bool, + desktop_flags: &DesktopFlags, ) -> Result<(), AnyError> { let app_name = bundle_path .file_name() @@ -1082,6 +1104,12 @@ fn make_self_extracting_dir( let hash = payload_hash(&payload)?; if windows { + let backend_args = format_backend_args_for_cmd(desktop_flags)?; + let launcher_args = if backend_args.is_empty() { + String::new() + } else { + format!(" {backend_args}") + }; let launcher = format!( "@echo off\r\n\ setlocal\r\n\ @@ -1091,10 +1119,16 @@ fn make_self_extracting_dir( \u{20} mkdir \"%DEST%\" 2>nul\r\n\ \u{20} tar -xf \"%DIR%{payload_name}\" -C \"%DEST%\"\r\n\ )\r\n\ - \"%DEST%\\{app_name}\\{app_name}.exe\" %*\r\n", + \"%DEST%\\{app_name}\\{app_name}.exe\"{launcher_args} %*\r\n", ); std::fs::write(bundle_path.join(format!("{app_name}.bat")), launcher)?; } else { + let backend_args = format_backend_args_for_shell(desktop_flags)?; + let launcher_args = if backend_args.is_empty() { + String::new() + } else { + format!(" {backend_args}") + }; let launcher = format!( "#!/bin/sh\n\ set -e\n\ @@ -1105,7 +1139,7 @@ fn make_self_extracting_dir( \u{20} mkdir -p \"$DEST\"\n\ \u{20} tar -xf \"$DIR/{payload_name}\" -C \"$DEST\"\n\ fi\n\ - exec \"$APP/{app_name}\" \"$@\"\n", + exec \"$APP/{app_name}\"{launcher_args} \"$@\"\n", ); let launcher_path = bundle_path.join(&app_name); std::fs::write(&launcher_path, launcher)?; @@ -1240,6 +1274,92 @@ async fn spawn_framework_dev_server( Ok((url, child)) } +/// Parses a raw `--backend-args` value into argv tokens using shell-style +/// quoting/escaping rules, so users can pass a single flag value containing +/// multiple backend args, some of which may themselves contain spaces (e.g. +/// `--backend-args '--user-agent="Custom Browser" --enable-x'`). +fn parse_backend_args(raw: &str) -> Result, AnyError> { + shell_words::split(raw).map_err(|e| { + deno_core::anyhow::anyhow!("invalid --backend-args quoting: {e}") + }) +} + +fn filtered_backend_args( + desktop_flags: &DesktopFlags, +) -> Result, AnyError> { + let Some(backend_args) = desktop_flags.backend_args.as_deref() else { + return Ok(Vec::new()); + }; + + let tokens = parse_backend_args(backend_args)?; + + let mut forwarded = Vec::new(); + let mut iter = tokens.into_iter().peekable(); + while let Some(token) = iter.next() { + match token.as_str() { + _ if token.starts_with("--remote-debugging-port") => { + log::warn!( + "Ignoring --remote-debugging-port in backend args; Deno handles the remote debugging port" + ); + } + _ if token == "--runtime" => { + log::warn!( + "Ignoring --runtime in backend args; Deno handles the runtime path" + ); + let _ = iter.next(); + } + _ => { + forwarded.push(token); + } + } + } + + Ok(forwarded) +} + +fn apply_backend_flags( + cmd: &mut std::process::Command, + desktop_flags: &DesktopFlags, +) -> Result<(), AnyError> { + for token in filtered_backend_args(desktop_flags)? { + cmd.arg(token); + } + Ok(()) +} + +fn format_backend_args_for_shell( + desktop_flags: &DesktopFlags, +) -> Result { + Ok( + filtered_backend_args(desktop_flags)? + .into_iter() + .map(|arg| { + shlex::try_quote(&arg) + .unwrap_or_else(|_| { + std::borrow::Cow::Owned(format!("\"{}\"", arg.replace('"', "\\\""))) + }) + .into_owned() + }) + .collect::>() + .join(" "), + ) +} + +fn format_backend_args_for_cmd( + desktop_flags: &DesktopFlags, +) -> Result { + Ok( + filtered_backend_args(desktop_flags)? + .into_iter() + .map(|arg| { + let escaped = arg.replace('%', "%%").replace('"', "\"\""); + format!("\"{escaped}\"") + }) + .collect::>() + .join(" "), + ) +} + /// Launch the desktop app with HMR enabled after compilation. /// /// Framework dev servers provide HMR via websocket. Since they run inside @@ -1340,6 +1460,8 @@ async fn run_desktop_hmr( cmd.env("DENO_DESKTOP_HMR", &source_abs); } + apply_backend_flags(&mut cmd, desktop_flags)?; + // In-runtime framework dev: the compiled entrypoint boots the framework's // dev server inside the desktop runtime (see `hmr_entrypoint_code`), so // server-side code keeps access to `Deno.desktop` (#35899). Signal @@ -1783,11 +1905,35 @@ async fn package_linux_app_dir( std::fs::copy(dylib_path, &dest_dylib)?; // Rename the LAUFEY backend binary to the app name so `` is the launcher - // the user runs directly — no `--runtime` argument and no shell wrapper. + // the user runs directly. When backend args are present, we add a tiny shell + // wrapper so those flags reach the backend while still pointing it at the + // colocated runtime. + let backend_args = format_backend_args_for_shell(desktop_flags)?; + let launcher_args = if backend_args.is_empty() { + String::new() + } else { + format!(" {backend_args}") + }; + let launcher_path = app_dir.join(&app_name); let staged_backend = app_dir.join(&laufey_binary_name); - if staged_backend != launcher_path { - std::fs::rename(&staged_backend, &launcher_path)?; + if backend_args.is_empty() { + if staged_backend != launcher_path { + std::fs::rename(&staged_backend, &launcher_path)?; + } + } else { + let wrapped_backend = app_dir.join(format!("{app_name}.bin")); + if staged_backend != wrapped_backend { + std::fs::rename(&staged_backend, &wrapped_backend)?; + } + let launcher = format!( + "#!/bin/sh\n\ + set -e\n\ + DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\n\ + export LAUFEY_RUNTIME_PATH=\"$DIR/{app_name}.so\"\n\ + exec \"$DIR/{app_name}.bin\"{launcher_args} \"$@\"\n", + ); + std::fs::write(&launcher_path, launcher)?; } #[cfg(unix)] { @@ -1878,19 +2024,29 @@ async fn package_linux_app_dir( /// are searched the same way the old sibling-checkout heuristic searched. const LAUFEY_DEV_DIR_ENV: &str = "LAUFEY_DEV_DIR"; +/// Overrides `/laufey` with a fixed cache directory. The backend +/// archives are 100+ MB, so downloading one per test is impractical when each +/// spec test gets its own throwaway `DENO_DIR` — the test harness points every +/// test at the same on-disk cache via this var instead (see +/// `tests/util/lib/builders.rs` and `tools/download_laufey.ts`). +const LAUFEY_CACHE_DIR_ENV: &str = "DENO_LAUFEY_CACHE_DIR"; + /// Resolves LAUFEY backend binaries and `.app` bundles, falling back to /// downloading prebuilt archives from the laufey GitHub releases when /// `LAUFEY_DEV_DIR` is not set. struct LaufeyBackendResolver { http_client_provider: Arc, - /// `/laufey//` + /// `/laufey//`, or `//` + /// when the override is set. cache_root: PathBuf, } impl LaufeyBackendResolver { fn new(factory: &CliFactory) -> Result { - let cache_root = - factory.deno_dir()?.root.join("laufey").join(LAUFEY_VERSION); + let cache_root = match std::env::var_os(LAUFEY_CACHE_DIR_ENV) { + Some(dir) => PathBuf::from(dir).join(LAUFEY_VERSION), + None => factory.deno_dir()?.root.join("laufey").join(LAUFEY_VERSION), + }; Ok(Self { http_client_provider: factory.http_client_provider().clone(), cache_root, @@ -3477,6 +3633,7 @@ fn create_linux_appimage( app_dir: &Path, appimage_path: &Path, target: Option<&str>, + desktop_flags: &DesktopFlags, ) -> Result<(), AnyError> { use std::io::Cursor; use std::io::Write as _; @@ -3502,10 +3659,16 @@ fn create_linux_appimage( // AppRun is what the AppImage invokes on launch. Thin shell shim that // delegates to the existing launcher (which already sets $DIR and execs // the backend with the right args). + let backend_args = format_backend_args_for_shell(desktop_flags)?; + let launcher_args = if backend_args.is_empty() { + String::new() + } else { + format!(" {backend_args}") + }; let apprun = format!( "#!/bin/sh\n\ DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\n\ - exec \"$DIR/{app_name}\" \"$@\"\n", + exec \"$DIR/{app_name}\"{launcher_args} \"$@\"\n", ); writer.push_file( Cursor::new(apprun.into_bytes()), @@ -5489,6 +5652,8 @@ mod disclaim_spawn { #[cfg(test)] mod tests { + use deno_config::deno_json::DesktopBackendArgsConfig; + use super::*; // --- macOS Info.plist --- @@ -6775,6 +6940,7 @@ def456 other.zip source_file: String::new(), output: None, args: vec![], + backend_args: None, target: None, icon: None, include: vec![], @@ -6802,11 +6968,37 @@ def456 other.zip #[test] fn appimage_uses_runtime_supported_zstd_squashfs() { + use crate::args::JavaScriptEngine; let tmp = tempfile::tempdir().unwrap(); let app_dir = fake_linux_app_dir(tmp.path(), "MyApp"); let appimage_path = tmp.path().join("MyApp.AppImage"); let target = Some("x86_64-unknown-linux-gnu"); - create_linux_appimage(&app_dir, &appimage_path, target).unwrap(); + create_linux_appimage( + &app_dir, + &appimage_path, + target, + &DesktopFlags { + source_file: ".".to_string(), + output: None, + args: Vec::new(), + backend_args: None, + target: None, + icon: None, + include: Vec::new(), + exclude: Vec::new(), + hmr: true, + backend: None, + all_targets: false, + identifier: None, + deep_links: Vec::new(), + codesign_identity: None, + inspect_renderer: None, + compress: None, + exclude_unused_npm: false, + engine: JavaScriptEngine::V8, + }, + ) + .unwrap(); let runtime_offset = appimage_runtime_for_target(target).unwrap().len() as u64; @@ -7591,4 +7783,130 @@ def456 other.zip // Left unset; callers fall back to "webview" via unwrap_or("webview"). assert_eq!(flags.backend.as_deref(), None); } + + // --- desktop.backendArgs config merge (CLI flag > deno.json) --- + + #[test] + fn backend_args_from_deno_json_for_selected_backend() { + let mut flags = DesktopFlags { + source_file: "main.ts".to_string(), + backend: Some("cef".to_string()), + backend_args: None, + ..Default::default() + }; + let config = DesktopConfig { + backend_args: Some(DesktopBackendArgsConfig { + cef: Some("--enable-logging".to_string()), + webview: Some("--verbose".to_string()), + }), + ..Default::default() + }; + apply_desktop_config_to_flags(&mut flags, config); + assert_eq!(flags.backend_args.as_deref(), Some("--enable-logging")); + } + + #[test] + fn backend_args_default_to_webview_when_backend_unset() { + let mut flags = DesktopFlags { + source_file: "main.ts".to_string(), + backend: None, + backend_args: None, + ..Default::default() + }; + let config = DesktopConfig { + backend_args: Some(DesktopBackendArgsConfig { + cef: Some("--enable-logging".to_string()), + webview: Some("--verbose".to_string()), + }), + ..Default::default() + }; + apply_desktop_config_to_flags(&mut flags, config); + assert_eq!(flags.backend_args.as_deref(), Some("--verbose")); + } + + #[test] + fn cli_backend_args_override_deno_json() { + let mut flags = DesktopFlags { + source_file: "main.ts".to_string(), + backend: Some("cef".to_string()), + backend_args: Some("--from-cli".to_string()), + ..Default::default() + }; + let config = DesktopConfig { + backend_args: Some(DesktopBackendArgsConfig { + cef: Some("--enable-logging".to_string()), + webview: None, + }), + ..Default::default() + }; + apply_desktop_config_to_flags(&mut flags, config); + assert_eq!(flags.backend_args.as_deref(), Some("--from-cli")); + } + + #[test] + fn backend_args_ignored_for_unknown_backend() { + let mut flags = DesktopFlags { + source_file: "main.ts".to_string(), + backend: Some("custom".to_string()), + backend_args: None, + ..Default::default() + }; + let config = DesktopConfig { + backend_args: Some(DesktopBackendArgsConfig { + cef: Some("--enable-logging".to_string()), + webview: Some("--verbose".to_string()), + }), + ..Default::default() + }; + apply_desktop_config_to_flags(&mut flags, config); + assert_eq!(flags.backend_args.as_deref(), None); + } + + // --- parse_backend_args: shell-style tokenization of --backend-args --- + + #[test] + fn parse_backend_args_simple_quoted_value() { + let tokens = parse_backend_args(r#"--user-agent="potato""#).unwrap(); + assert_eq!(tokens, vec!["--user-agent=potato".to_string()]); + } + + #[test] + fn parse_backend_args_quoted_value_with_spaces() { + let tokens = + parse_backend_args(r#"--user-agent="potato browser" --enable-logging"#) + .unwrap(); + assert_eq!( + tokens, + vec![ + "--user-agent=potato browser".to_string(), + "--enable-logging".to_string(), + ] + ); + } + + #[test] + fn parse_backend_args_preserves_escaped_inner_quotes() { + let tokens = parse_backend_args(r#"--title="a \"quoted\" title""#).unwrap(); + assert_eq!(tokens, vec![r#"--title=a "quoted" title"#.to_string()]); + } + + #[test] + fn parse_backend_args_simple_unquoted_value_unaffected() { + let tokens = parse_backend_args("--enable-logging --verbose").unwrap(); + assert_eq!( + tokens, + vec!["--enable-logging".to_string(), "--verbose".to_string()] + ); + } + + #[test] + fn parse_backend_args_malformed_quoting_errors() { + let err = parse_backend_args(r#"--user-agent="unterminated"#).unwrap_err(); + assert!( + err + .to_string() + .starts_with("invalid --backend-args quoting: "), + "unexpected error message: {err}", + ); + } } diff --git a/libs/cli_parser/src/convert.rs b/libs/cli_parser/src/convert.rs index 5ea827570eeada..c09cd070a6c8ae 100644 --- a/libs/cli_parser/src/convert.rs +++ b/libs/cli_parser/src/convert.rs @@ -2726,6 +2726,7 @@ fn desktop_parse( inspect_renderer, compress, exclude_unused_npm: result.get_bool("exclude-unused-npm"), + backend_args: result.get_one("backend-args").map(|s| s.to_string()), engine: result .get_one("engine") .map(|value| value.parse().expect("engine is validated by the parser")) diff --git a/libs/cli_parser/src/defs.rs b/libs/cli_parser/src/defs.rs index c12cfddda2811b..d81f21a1442569 100644 --- a/libs/cli_parser/src/defs.rs +++ b/libs/cli_parser/src/defs.rs @@ -3136,6 +3136,11 @@ pub static DESKTOP_SUBCOMMAND: CommandDef = CommandDef { .num_args(NumArgs::Optional) .require_equals() .help("Load environment variables from local file\n Only the first environment variable with a given key is used.\n Existing process environment variables are not overwritten, so if variables with the same names already exist in the environment, their values will be preserved.\n Where multiple declarations for the same environment variable exist in your .env file, the first one encountered is applied. This is determined by the order of the files you pass as arguments."), + ArgDef::new("backend-args") + .long("backend-args") + .action(ArgAction::Set) + .num_args(NumArgs::Exact(1)) +.help("Command-Line arguments for the chosen backend") ], arg_groups: &[ UNSTABLE_DEPRECATED_ARG, UNSTABLE_FEATURE_ARGS, diff --git a/libs/cli_parser/src/flags.rs b/libs/cli_parser/src/flags.rs index aa1354c434e88b..5c2e6ee9be3259 100644 --- a/libs/cli_parser/src/flags.rs +++ b/libs/cli_parser/src/flags.rs @@ -280,6 +280,8 @@ pub struct DesktopFlags { /// the full managed snapshot. Same opt-in semantics as /// `deno compile --exclude-unused-npm`. pub exclude_unused_npm: bool, + /// Optional flags to be passed to the chosen laufey backend + pub backend_args: Option, } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/libs/config/deno_json/mod.rs b/libs/config/deno_json/mod.rs index 5d733e5f0bee06..877d9066829e23 100644 --- a/libs/config/deno_json/mod.rs +++ b/libs/config/deno_json/mod.rs @@ -925,6 +925,13 @@ struct SerializedDesktopAppConfig { pub deep_links: Option>, } +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +#[serde(default, deny_unknown_fields)] +struct SerializedDesktopBackendArgsConfig { + pub cef: Option, + pub webview: Option, +} + #[derive(Clone, Debug, Default, Deserialize, PartialEq)] #[serde(default, deny_unknown_fields)] struct SerializedDesktopOutputConfig { @@ -962,6 +969,8 @@ struct SerializedDesktopMacOSConfig { struct SerializedDesktopConfig { pub app: Option, pub backend: Option, + #[serde(alias = "backendArgs")] + pub backend_args: Option, pub output: Option, pub release: Option, #[serde(rename = "errorReporting")] @@ -1005,6 +1014,10 @@ impl SerializedDesktopConfig { }), }), backend: self.backend, + backend_args: self.backend_args.map(|o| DesktopBackendArgsConfig { + cef: o.cef, + webview: o.webview, + }), output: self.output.map(|o| DesktopOutputConfig { macos: o.macos, windows: o.windows, @@ -1052,6 +1065,12 @@ pub struct DesktopAppConfig { pub deep_links: Option>, } +#[derive(Clone, Debug, Default, PartialEq)] +pub struct DesktopBackendArgsConfig { + pub cef: Option, + pub webview: Option, +} + #[derive(Clone, Debug, Default, PartialEq)] pub struct DesktopOutputConfig { pub macos: Option, @@ -1078,6 +1097,7 @@ pub struct DesktopMacOSConfig { pub struct DesktopConfig { pub app: Option, pub backend: Option, + pub backend_args: Option, pub output: Option, pub release: Option, pub error_reporting: Option, @@ -2886,6 +2906,30 @@ mod tests { assert!(error.to_string().contains("404.json")); } + #[test] + fn desktop_config_accepts_backend_args_camel_case() { + let config_text = r#"{ + "desktop": { + "backend": "cef", + "backendArgs": { + "cef": "--user-agent=potato" + } + } + }"#; + let config_specifier = Url::parse("file:///deno/deno.json").unwrap(); + let config_file = ConfigFile::new(config_text, config_specifier).unwrap(); + + let desktop_config = config_file.to_desktop_config().unwrap(); + assert_eq!(desktop_config.backend, Some("cef".to_string())); + assert_eq!( + desktop_config.backend_args, + Some(DesktopBackendArgsConfig { + cef: Some("--user-agent=potato".to_string()), + webview: None, + }) + ); + } + #[test] fn test_parse_config() { let config_text = r#"{ diff --git a/libs/config/workspace/mod.rs b/libs/config/workspace/mod.rs index 0fddfd4ec720d2..81ad325b51938b 100644 --- a/libs/config/workspace/mod.rs +++ b/libs/config/workspace/mod.rs @@ -2572,6 +2572,7 @@ impl WorkspaceDirectory { Ok(DesktopConfig { app: member_config.app.or(root_config.app), backend: member_config.backend.or(root_config.backend), + backend_args: member_config.backend_args.or(root_config.backend_args), output: member_config.output.or(root_config.output), release: member_config.release.or(root_config.release), error_reporting: member_config diff --git a/tests/specs/desktop/backend_args/__test__.jsonc b/tests/specs/desktop/backend_args/__test__.jsonc new file mode 100644 index 00000000000000..6100bef7914245 --- /dev/null +++ b/tests/specs/desktop/backend_args/__test__.jsonc @@ -0,0 +1,258 @@ +{ + "tempDir": true, + // Kill any step that doesn't finish in 60s instead of hanging until CI + // cancels the job. The steps that launch a packaged app only exit when the + // GUI backend makes its HTTP request; if the backend can't start (e.g. no + // usable display/sandbox on a CI runner) the process would otherwise run + // forever. + "timeout": 60, + "tests": { + "forwards_backend_flags_to_launcher": { + "steps": [ + { + "if": "linux", + "args": [ + "desktop", + "--backend=cef", + "--backend-args=--user-agent=\"potato\"", + "--output", + "./hello", + "./main.ts" + ], + "output": "[WILDCARD]" + }, + { + "if": "linux", + "commandName": "./hello/hello", + "args": [], + "output": "desktop.out" + }, + { + // laufey v0.6.1 ships no cef build for aarch64-pc-windows-msvc, + // so Windows exercises the webview backend instead. Unlike the + // cef backend (which feeds the process argv into CefMainArgs so + // extra switches reach Chromium), the webview backend parses only + // `--runtime` and silently drops everything else — so the packaged + // app's webview would keep its default user agent and the app's + // `main.ts` would never see a user-agent header and exit. Only + // assert packaging (which is where backend args are parsed and + // baked into the launcher), never launch the packaged app here. + "if": "windows", + "args": [ + "desktop", + "--backend=webview", + "--backend-args=--user-agent=\"potato\"", + "--output", + "./hello", + "./main.ts" + ], + "output": "[WILDCARD]" + } + ] + }, + "forwards_backend_flags_to_launcher_hmi": { + "steps": [ + { + "if": "unix", + "args": [ + "desktop", + "--hmr", + "--backend=cef", + "--backend-args=--user-agent=\"potato\"", + "--output", + "./hello3", + "./main.ts" + ], + "output": "desktop_hmi.out" + } + ] + }, + "forwards_backend_flags_with_quoted_spaces": { + "steps": [ + { + "if": "linux", + "args": [ + "desktop", + "--backend=cef", + "--backend-args=--user-agent=\"potato browser\" --enable-logging", + "--output", + "./hello_spaces", + "./main.ts" + ], + "output": "[WILDCARD]" + }, + { + "if": "linux", + "commandName": "./hello_spaces/hello_spaces", + "args": [], + "output": "desktop_spaces.out" + }, + { + // Windows (webview): packaging only, see + // forwards_backend_flags_to_launcher. + "if": "windows", + "args": [ + "desktop", + "--backend=webview", + "--backend-args=--user-agent=\"potato browser\" --enable-logging", + "--output", + "./hello_spaces", + "./main.ts" + ], + "output": "[WILDCARD]" + } + ] + }, + "forwards_backend_flags_with_escaped_quotes": { + "steps": [ + { + "if": "linux", + "args": [ + "desktop", + "--backend=cef", + "--backend-args=--user-agent=\"a \\\"quoted\\\" agent\"", + "--output", + "./hello_escaped", + "./main.ts" + ], + "output": "[WILDCARD]" + }, + { + "if": "linux", + "commandName": "./hello_escaped/hello_escaped", + "args": [], + "output": "desktop_escaped.out" + }, + { + // Windows (webview): packaging only, see + // forwards_backend_flags_to_launcher. + "if": "windows", + "args": [ + "desktop", + "--backend=webview", + "--backend-args=--user-agent=\"a \\\"quoted\\\" agent\"", + "--output", + "./hello_escaped", + "./main.ts" + ], + "output": "[WILDCARD]" + } + ] + }, + "malformed_backend_args_reports_error": { + "steps": [ + { + "if": "linux", + "args": [ + "desktop", + "--backend=cef", + "--backend-args=--user-agent=\"unterminated", + "--output", + "./hello_malformed", + "./main.ts" + ], + "output": "[WILDCARD]invalid --backend-args quoting[WILDCARD]", + "exitCode": 1 + }, + { + "if": "mac", + "args": [ + "desktop", + "--hmr", + "--backend=cef", + "--backend-args=--user-agent=\"unterminated", + "--output", + "./hello_malformed", + "./main.ts" + ], + "output": "[WILDCARD]invalid --backend-args quoting[WILDCARD]", + "exitCode": 1 + }, + { + // Mirror the mac variant: --hmr makes the malformed quoting fail + // fast at dev-server startup, before any backend download/launch. + "if": "windows", + "args": [ + "desktop", + "--hmr", + "--backend=webview", + "--backend-args=--user-agent=\"unterminated", + "--output", + "./hello_malformed", + "./main.ts" + ], + "output": "[WILDCARD]invalid --backend-args quoting[WILDCARD]", + "exitCode": 1 + } + ] + }, + "forwards_backend_flags_to_launcher_config": { + "steps": [ + { + "if": "linux", + "args": [ + "desktop", + "--output", + "./hello2", + "--config", + "./desktop_config.jsonc", + "./main.ts" + ], + "output": "[WILDCARD]" + }, + { + "if": "linux", + "commandName": "./hello2/hello2", + "args": [], + "output": "desktop_config.out" + }, + { + // Windows (webview): packaging only, see + // forwards_backend_flags_to_launcher. + "if": "windows", + "args": [ + "desktop", + "--output", + "./hello2", + "--config", + "./desktop_config_windows.jsonc", + "./main.ts" + ], + "output": "[WILDCARD]" + } + ] + }, + "packages_macos_app_bundle": { + "steps": [ + { + "if": "mac", + "args": [ + "desktop", + "--backend=cef", + "--backend-args=--user-agent=\"potato\"", + "--output", + "./hello", + "./main.ts" + ], + "output": "desktop_mac.out" + } + ] + }, + "packages_macos_app_bundle_config": { + "steps": [ + { + "if": "mac", + "args": [ + "desktop", + "--output", + "./hello2", + "--config", + "./desktop_config.jsonc", + "./main.ts" + ], + "output": "desktop_mac_config.out" + } + ] + } + } +} diff --git a/tests/specs/desktop/backend_args/desktop.out b/tests/specs/desktop/backend_args/desktop.out new file mode 100644 index 00000000000000..8d7da73430c920 --- /dev/null +++ b/tests/specs/desktop/backend_args/desktop.out @@ -0,0 +1,5 @@ +Runtime loaded successfully from: [WILDCARD]/hello/hello.so +Runtime started +[desktop] dylib path: "[WILDCARD]/hello/hello.so" +Listening on http://127.0.0.1:[WILDCARD]/ +potato diff --git a/tests/specs/desktop/backend_args/desktop_config.jsonc b/tests/specs/desktop/backend_args/desktop_config.jsonc new file mode 100644 index 00000000000000..66934962012917 --- /dev/null +++ b/tests/specs/desktop/backend_args/desktop_config.jsonc @@ -0,0 +1,8 @@ +{ + "desktop": { + "backend": "cef", + "backendArgs": { + "cef": "--user-agent=potato" + } + } +} diff --git a/tests/specs/desktop/backend_args/desktop_config.out b/tests/specs/desktop/backend_args/desktop_config.out new file mode 100644 index 00000000000000..7719b7da82a46b --- /dev/null +++ b/tests/specs/desktop/backend_args/desktop_config.out @@ -0,0 +1,5 @@ +Runtime loaded successfully from: [WILDCARD]/hello2/hello2.so +Runtime started +[desktop] dylib path: "[WILDCARD]/hello2/hello2.so" +Listening on http://127.0.0.1:[WILDCARD]/ +potato diff --git a/tests/specs/desktop/backend_args/desktop_config_windows.jsonc b/tests/specs/desktop/backend_args/desktop_config_windows.jsonc new file mode 100644 index 00000000000000..10401674e9c409 --- /dev/null +++ b/tests/specs/desktop/backend_args/desktop_config_windows.jsonc @@ -0,0 +1,8 @@ +{ + "desktop": { + "backend": "webview", + "backendArgs": { + "webview": "--user-agent=potato" + } + } +} diff --git a/tests/specs/desktop/backend_args/desktop_escaped.out b/tests/specs/desktop/backend_args/desktop_escaped.out new file mode 100644 index 00000000000000..6f8a847f87c2cf --- /dev/null +++ b/tests/specs/desktop/backend_args/desktop_escaped.out @@ -0,0 +1,5 @@ +Runtime loaded successfully from: [WILDCARD]/hello_escaped/hello_escaped.so +Runtime started +[desktop] dylib path: "[WILDCARD]/hello_escaped/hello_escaped.so" +Listening on http://127.0.0.1:[WILDCARD]/ +a "quoted" agent diff --git a/tests/specs/desktop/backend_args/desktop_hmi.out b/tests/specs/desktop/backend_args/desktop_hmi.out new file mode 100644 index 00000000000000..620778c9954d73 --- /dev/null +++ b/tests/specs/desktop/backend_args/desktop_hmi.out @@ -0,0 +1,6 @@ +[WILDCARD] +Runtime loaded successfully from: [WILDCARD]/hello3.so +Runtime started +[desktop] dylib path: "[WILDCARD]/hello3.so" +Listening on http://127.0.0.1:[WILDCARD]/ +potato diff --git a/tests/specs/desktop/backend_args/desktop_mac.out b/tests/specs/desktop/backend_args/desktop_mac.out new file mode 100644 index 00000000000000..e3efb696823d53 --- /dev/null +++ b/tests/specs/desktop/backend_args/desktop_mac.out @@ -0,0 +1 @@ +[WILDCARD]Bundle[WILDCARD]hello.app[WILDCARD] diff --git a/tests/specs/desktop/backend_args/desktop_mac_config.out b/tests/specs/desktop/backend_args/desktop_mac_config.out new file mode 100644 index 00000000000000..9bf31f137ffc4e --- /dev/null +++ b/tests/specs/desktop/backend_args/desktop_mac_config.out @@ -0,0 +1 @@ +[WILDCARD]Bundle[WILDCARD]hello2.app[WILDCARD] diff --git a/tests/specs/desktop/backend_args/desktop_spaces.out b/tests/specs/desktop/backend_args/desktop_spaces.out new file mode 100644 index 00000000000000..802bd9c76b03e3 --- /dev/null +++ b/tests/specs/desktop/backend_args/desktop_spaces.out @@ -0,0 +1,5 @@ +Runtime loaded successfully from: [WILDCARD]/hello_spaces/hello_spaces.so +Runtime started +[desktop] dylib path: "[WILDCARD]/hello_spaces/hello_spaces.so" +Listening on http://127.0.0.1:[WILDCARD]/ +potato browser diff --git a/tests/specs/desktop/backend_args/main.ts b/tests/specs/desktop/backend_args/main.ts new file mode 100644 index 00000000000000..5d7a95904e587a --- /dev/null +++ b/tests/specs/desktop/backend_args/main.ts @@ -0,0 +1,11 @@ +Deno.serve((req) => { + const userAgent = req.headers.get("user-agent"); + if (userAgent) { + console.log(userAgent); + Deno.exit(); + } else { + return new Response("", { + headers: { "content-type": "text/html" }, + }); + } +}); diff --git a/tests/util/lib/builders.rs b/tests/util/lib/builders.rs index 9a9c38a2c79125..4f3edbbf2acc65 100644 --- a/tests/util/lib/builders.rs +++ b/tests/util/lib/builders.rs @@ -304,6 +304,22 @@ impl TestContextBuilder { } } + // Same idea as `DENO_TSC_BIN` above, but for `deno desktop`'s "laufey" + // backend downloads: point every test's fresh `DENO_DIR` at one shared + // cache dir instead of re-downloading the (100+ MB) backend archive per + // test. An explicit ambient `DENO_LAUFEY_CACHE_DIR` still wins, as does a + // value a test sets itself. + if !envs.contains_key("DENO_LAUFEY_CACHE_DIR") { + let laufey_cache_dir = std::env::var_os("DENO_LAUFEY_CACHE_DIR") + .map(|v| v.to_string_lossy().into_owned()) + .unwrap_or_else(|| { + crate::native_laufey_cache_dir() + .to_string_lossy() + .into_owned() + }); + envs.insert("DENO_LAUFEY_CACHE_DIR".to_string(), laufey_cache_dir); + } + TestContext { cwd, deno_exe, diff --git a/tests/util/lib/lib.rs b/tests/util/lib/lib.rs index d1db9e00ac65f7..54031271356110 100644 --- a/tests/util/lib/lib.rs +++ b/tests/util/lib/lib.rs @@ -192,6 +192,10 @@ pub fn native_tsc_bin_path() -> Option { None } +pub fn native_laufey_cache_dir() -> PathRef { + root_path().join("target").join(".native_laufey") +} + pub fn prebuilt_path() -> PathRef { third_party_path().join("prebuilt") } diff --git a/tools/deno.lock.json b/tools/deno.lock.json index 9dd838ae904bab..571e55f037dc02 100644 --- a/tools/deno.lock.json +++ b/tools/deno.lock.json @@ -4,9 +4,13 @@ "jsr:@b-fuze/deno-dom@0.1.56": "0.1.56", "jsr:@david/console-static-text@0.3": "0.3.4", "jsr:@david/dax@~0.43.2": "0.43.2", + "jsr:@david/gagen@0.3.1": "0.3.1", "jsr:@david/path@0.2": "0.2.0", "jsr:@david/which@~0.4.1": "0.4.2", "jsr:@deno/rust-automation@0.22.2": "0.22.2", + "jsr:@std/collections@^1.1.3": "1.3.0", + "jsr:@std/toml@1": "1.0.11", + "jsr:@std/yaml@^1.0.11": "1.1.2", "npm:decompress@4.2.1": "4.2.1", "npm:octokit@^5.0.3": "5.0.5" }, @@ -25,6 +29,12 @@ "jsr:@david/which" ] }, + "@david/gagen@0.3.1": { + "integrity": "888b90d356e6530f24d9c9ec5beff1f0d0565d44293b356cd6844768ffe31780", + "dependencies": [ + "jsr:@std/yaml" + ] + }, "@david/path@0.2.0": { "integrity": "f2d7aa7f02ce5a55e27c09f9f1381794acb09d328f8d3c8a2e3ab3ffc294dccd" }, @@ -37,6 +47,18 @@ "jsr:@david/dax", "npm:octokit" ] + }, + "@std/collections@1.3.0": { + "integrity": "eb36b43d784477ea0b476483ac034a14bdd182aff921c812ecf662a1fcef9498" + }, + "@std/toml@1.0.11": { + "integrity": "e084988b872ca4bad6aedfb7350f6eeed0e8ba88e9ee5e1590621c5b5bb8f715", + "dependencies": [ + "jsr:@std/collections" + ] + }, + "@std/yaml@1.1.2": { + "integrity": "de612653c036749ba2044165e316f52412b0f0698afffb7ee80b5331d6d2abae" } }, "npm": { @@ -689,51 +711,5 @@ "fd-slicer" ] } - }, - "workspace": { - "links": { - "jsr:@std/assert@1.0.19": {}, - "jsr:@std/async@1.2.0": {}, - "jsr:@std/bytes@1.0.6": {}, - "jsr:@std/cache@0.2.2": {}, - "jsr:@std/cbor@0.1.9": {}, - "jsr:@std/cli@1.0.28": {}, - "jsr:@std/collections@1.1.6": {}, - "jsr:@std/crypto@1.0.5": {}, - "jsr:@std/csv@1.0.6": {}, - "jsr:@std/data-structures@1.0.10": {}, - "jsr:@std/datetime@0.225.7": {}, - "jsr:@std/dotenv@0.225.6": {}, - "jsr:@std/encoding@1.0.10": {}, - "jsr:@std/expect@1.0.18": {}, - "jsr:@std/fmt@1.0.9": {}, - "jsr:@std/front-matter@1.0.9": {}, - "jsr:@std/fs@1.0.23": {}, - "jsr:@std/html@1.0.5": {}, - "jsr:@std/http@1.0.25": {}, - "jsr:@std/ini@1.0.0-rc.9": {}, - "jsr:@std/internal@1.0.12": {}, - "jsr:@std/io@0.225.3": {}, - "jsr:@std/json@1.0.3": {}, - "jsr:@std/jsonc@1.0.2": {}, - "jsr:@std/math@0.0.0": {}, - "jsr:@std/media-types@1.1.0": {}, - "jsr:@std/msgpack@1.0.3": {}, - "jsr:@std/net@1.0.6": {}, - "jsr:@std/path@1.1.4": {}, - "jsr:@std/random@0.1.5": {}, - "jsr:@std/regexp@1.0.1": {}, - "jsr:@std/semver@1.0.8": {}, - "jsr:@std/streams@1.0.17": {}, - "jsr:@std/tar@0.1.10": {}, - "jsr:@std/testing@1.0.17": {}, - "jsr:@std/text@1.0.17": {}, - "jsr:@std/toml@1.0.11": {}, - "jsr:@std/ulid@1.0.0": {}, - "jsr:@std/uuid@1.1.0": {}, - "jsr:@std/webgpu@0.224.9": {}, - "jsr:@std/xml@0.1.0": {}, - "jsr:@std/yaml@1.0.12": {} - } } } diff --git a/tools/download_laufey.ts b/tools/download_laufey.ts new file mode 100644 index 00000000000000..66aab17add15e6 --- /dev/null +++ b/tools/download_laufey.ts @@ -0,0 +1,284 @@ +#!/usr/bin/env -S deno run --allow-read --allow-write --allow-net --allow-run +// Copyright 2018-2026 the Deno authors. MIT license. +// deno-lint-ignore-file no-console + +// Pre-downloads a pinned laufey desktop backend archive into the shared cache +// layout `deno desktop` expects, so CI (and local test runs) don't re-download +// the backend for every test's fresh `DENO_DIR`. +// +// deno run -A tools/download_laufey.ts [backend] [cache_dir] +// +// The default backend stays `cef`, except on Windows where it stays `webview` +// because laufey v0.6.1 does not ship a `cef` build for +// `aarch64-pc-windows-msvc`. + +const LAUFEY_SUMS_URL = new URL("../cli/laufey_sums.lock", import.meta.url); + +function absolute(path: string): string { + if (path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path)) { + return path; + } + return `${Deno.cwd()}/${path}`; +} + +async function exists(path: string): Promise { + try { + await Deno.stat(path); + return true; + } catch (error) { + if (error instanceof Deno.errors.NotFound) { + return false; + } + throw error; + } +} + +export function defaultBackendFor(os: string): string { + return os === "windows" ? "webview" : "cef"; +} + +export function parsePinnedVersion(contents: string): string { + const match = contents.match(/^# version: v([0-9][^\s]*)$/m); + if (match == null) { + throw new Error( + "cli/laufey_sums.lock is missing a '# version: vX.Y.Z' directive", + ); + } + return match[1]; +} + +export function parsePinnedSha256( + contents: string, + archive: string, +): string | null { + for (const line of contents.split("\n")) { + const match = line.match(/^([0-9a-fA-F]+)\s+\*?(\S+)$/); + if (match?.[2] === archive) { + return match[1]; + } + } + return null; +} + +export function laufeyTargetForBuild( + build: Pick, +): string { + const arch = build.arch; + if (arch !== "x86_64" && arch !== "aarch64") { + throw new Error(`unsupported laufey architecture: ${arch}`); + } + + const os = (() => { + switch (build.os) { + case "linux": + return "unknown-linux-gnu"; + case "darwin": + return "apple-darwin"; + case "windows": + return "pc-windows-msvc"; + default: + throw new Error(`unsupported laufey operating system: ${build.os}`); + } + })(); + + return `${arch}-${os}`; +} + +export function laufeyArchiveName(backend: string, target: string): string { + const archiveBackend = backend === "raw" ? "winit" : backend; + const extension = target.includes("windows") ? "zip" : "tar.gz"; + return `laufey-${archiveBackend}-${target}.${extension}`; +} + +export function laufeyReleaseUrl(version: string, archive: string): string { + return `https://github.com/littledivy/laufey/releases/download/v${version}/${archive}`; +} + +type DownloadPlan = { + archive: string; + markerPath: string; + parentDir: string; + sha256: string; + target: string; + targetDir: string; + url: string; + version: string; +}; + +export function buildDownloadPlan( + backend: string, + cacheDir: string, + build: Pick, + lockContents: string, +): DownloadPlan { + const version = parsePinnedVersion(lockContents); + const target = laufeyTargetForBuild(build); + const archive = laufeyArchiveName(backend, target); + const sha256 = parsePinnedSha256(lockContents, archive); + if (sha256 == null) { + throw new Error( + `no pinned SHA-256 for ${archive} in cli/laufey_sums.lock`, + ); + } + const targetDir = `${cacheDir}/${version}/${backend}/${target}`; + return { + archive, + markerPath: `${targetDir}/.downloaded`, + parentDir: `${cacheDir}/${version}/${backend}`, + sha256: sha256.toLowerCase(), + target, + targetDir, + url: laufeyReleaseUrl(version, archive), + version, + }; +} + +async function sha256Hex(data: Uint8Array): Promise { + const digest = await crypto.subtle.digest("SHA-256", Uint8Array.from(data)); + return Array.from( + new Uint8Array(digest), + (byte) => byte.toString(16).padStart(2, "0"), + ).join(""); +} + +async function downloadArchive(url: string): Promise { + const response = await fetch(url, { + headers: { + "user-agent": `deno-desktop/${Deno.version.deno} (+https://deno.com)`, + }, + redirect: "follow", + }); + if (!response.ok) { + throw new Error( + `failed to download ${url}: ${response.status} ${response.statusText}`, + ); + } + const data = new Uint8Array(await response.arrayBuffer()); + if (data.length === 0) { + throw new Error(`empty response from ${url}`); + } + return data; +} + +async function runCommand(command: string, args: string[]): Promise { + const output = await new Deno.Command(command, { + args, + stderr: "inherit", + stdout: "inherit", + }).output(); + if (!output.success) { + throw new Error(`command failed: ${command} ${args.join(" ")}`); + } +} + +async function expandZipWithPowerShell( + archivePath: string, + destination: string, +): Promise { + const quotedArchivePath = archivePath.replaceAll("'", "''"); + const quotedDestination = destination.replaceAll("'", "''"); + const command = `Expand-Archive -LiteralPath '${quotedArchivePath}' ` + + `-DestinationPath '${quotedDestination}' -Force`; + for (const shell of ["pwsh", "powershell"]) { + try { + await runCommand(shell, ["-NoLogo", "-NoProfile", "-Command", command]); + return; + } catch (error) { + if (error instanceof Deno.errors.NotFound) { + continue; + } + } + } + throw new Error("could not find PowerShell to extract laufey zip archive"); +} + +async function extractArchive( + archivePath: string, + archive: string, + destination: string, +): Promise { + if (archive.endsWith(".tar.gz")) { + await runCommand("tar", ["-xzf", archivePath, "-C", destination]); + return; + } + if (archive.endsWith(".zip")) { + try { + await runCommand("tar", ["-xf", archivePath, "-C", destination]); + } catch (error) { + if (Deno.build.os !== "windows") { + throw error; + } + await expandZipWithPowerShell(archivePath, destination); + } + return; + } + throw new Error(`unsupported laufey archive format: ${archive}`); +} + +async function ensureDownloaded(plan: DownloadPlan): Promise { + if (await exists(plan.markerPath)) { + return; + } + + await Deno.mkdir(plan.parentDir, { recursive: true }); + const staging = await Deno.makeTempDir({ + dir: plan.parentDir, + prefix: ".staging-", + }); + + let renamed = false; + try { + const data = await downloadArchive(plan.url); + const actual = await sha256Hex(data); + if (actual !== plan.sha256) { + throw new Error( + `checksum mismatch for ${plan.archive} (downloaded from ${plan.url})\n` + + ` expected: ${plan.sha256}\n` + + ` actual: ${actual}`, + ); + } + + const archivePath = `${staging}/${plan.archive}`; + await Deno.writeFile(archivePath, data); + await extractArchive(archivePath, plan.archive, staging); + await Deno.remove(archivePath); + await Deno.writeTextFile(`${staging}/.downloaded`, `v${plan.version}\n`); + + if (await exists(plan.markerPath)) { + return; + } + + if (await exists(plan.targetDir)) { + await Deno.remove(plan.targetDir, { recursive: true }); + } + + try { + await Deno.rename(staging, plan.targetDir); + renamed = true; + } catch (error) { + if (await exists(plan.markerPath)) { + return; + } + throw error; + } + } finally { + if (!renamed) { + await Deno.remove(staging, { recursive: true }).catch(() => {}); + } + } +} + +async function main() { + const backend = Deno.args[0] ?? defaultBackendFor(Deno.build.os); + const cacheDir = absolute(Deno.args[1] ?? "./target/.native_laufey"); + const lockContents = await Deno.readTextFile(LAUFEY_SUMS_URL); + const plan = buildDownloadPlan(backend, cacheDir, Deno.build, lockContents); + + console.error(`Downloading laufey "${backend}" backend into ${cacheDir}...`); + await ensureDownloaded(plan); + console.log(cacheDir); +} + +if (import.meta.main) { + await main(); +} diff --git a/tools/download_laufey_test.ts b/tools/download_laufey_test.ts new file mode 100644 index 00000000000000..0534173bb6dc61 --- /dev/null +++ b/tools/download_laufey_test.ts @@ -0,0 +1,129 @@ +// Copyright 2018-2026 the Deno authors. MIT license. + +import { + buildDownloadPlan, + defaultBackendFor, + laufeyArchiveName, + laufeyTargetForBuild, + parsePinnedSha256, + parsePinnedVersion, +} from "./download_laufey.ts"; + +function assertEquals(actual: T, expected: T, message?: string) { + if (actual !== expected) { + throw new Error( + `${ + message ?? "values are not equal" + }\nexpected: ${expected}\nactual: ${actual}`, + ); + } +} + +function assertThrows(fn: () => unknown, expectedText: string) { + try { + fn(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (message.includes(expectedText)) { + return; + } + throw new Error( + `expected error containing '${expectedText}', got '${message}'`, + ); + } + throw new Error(`expected error containing '${expectedText}'`); +} + +Deno.test("parsePinnedVersion reads lockfile directive", () => { + assertEquals( + parsePinnedVersion("# comment\n# version: v0.6.1\n"), + "0.6.1", + ); +}); + +Deno.test("parsePinnedVersion rejects missing directive", () => { + assertThrows( + () => + parsePinnedVersion( + "95b7dad3 laufey-cef-x86_64-unknown-linux-gnu.tar.gz", + ), + "missing a '# version: vX.Y.Z' directive", + ); +}); + +Deno.test("parsePinnedSha256 supports GNU sha256sum formats", () => { + const contents = [ + "abc123 laufey-cef-x86_64-unknown-linux-gnu.tar.gz", + "def456 *laufey-webview-x86_64-pc-windows-msvc.zip", + ].join("\n"); + assertEquals( + parsePinnedSha256( + contents, + "laufey-cef-x86_64-unknown-linux-gnu.tar.gz", + ), + "abc123", + ); + assertEquals( + parsePinnedSha256( + contents, + "laufey-webview-x86_64-pc-windows-msvc.zip", + ), + "def456", + ); +}); + +Deno.test("laufeyTargetForBuild maps supported targets", () => { + assertEquals( + laufeyTargetForBuild({ arch: "x86_64", os: "linux" }), + "x86_64-unknown-linux-gnu", + ); + assertEquals( + laufeyTargetForBuild({ arch: "aarch64", os: "darwin" }), + "aarch64-apple-darwin", + ); + assertEquals( + laufeyTargetForBuild({ arch: "x86_64", os: "windows" }), + "x86_64-pc-windows-msvc", + ); +}); + +Deno.test("archive naming keeps raw cache key but uses winit upstream name", () => { + assertEquals(defaultBackendFor("windows"), "webview"); + assertEquals(defaultBackendFor("linux"), "cef"); + assertEquals( + laufeyArchiveName("raw", "x86_64-unknown-linux-gnu"), + "laufey-winit-x86_64-unknown-linux-gnu.tar.gz", + ); + assertEquals( + laufeyArchiveName("webview", "x86_64-pc-windows-msvc"), + "laufey-webview-x86_64-pc-windows-msvc.zip", + ); +}); + +Deno.test("buildDownloadPlan matches resolver cache layout", () => { + const plan = buildDownloadPlan( + "raw", + "/tmp/native_laufey", + { arch: "x86_64", os: "linux" }, + [ + "# version: v0.6.1", + "b0db0c0892181481976da48291ff5befe1cdf81d6e1e2598d6c78b9ed2c616f5 laufey-winit-x86_64-unknown-linux-gnu.tar.gz", + ].join("\n"), + ); + + assertEquals(plan.version, "0.6.1"); + assertEquals(plan.target, "x86_64-unknown-linux-gnu"); + assertEquals(plan.archive, "laufey-winit-x86_64-unknown-linux-gnu.tar.gz"); + assertEquals( + plan.url, + "https://github.com/littledivy/laufey/releases/download/v0.6.1/laufey-winit-x86_64-unknown-linux-gnu.tar.gz", + ); + assertEquals( + plan.targetDir, + "/tmp/native_laufey/0.6.1/raw/x86_64-unknown-linux-gnu", + ); + assertEquals( + plan.markerPath, + "/tmp/native_laufey/0.6.1/raw/x86_64-unknown-linux-gnu/.downloaded", + ); +});