mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 02:36:01 -10:00
Merge pull request #12272 from esengine/feat/studio-windows-arm64-ci-build
ci(studio): build and smoke-test a native Windows ARM64 package
This commit is contained in:
5 files changed
+302
-20
No files matched your search
+145
-12
@@ -121,6 +121,45 @@ jobs:
|
||||
CGO_ENABLED=0 GOOS=$os GOARCH=amd64 go build -o /dev/null ./cmd/reasonix-studio-host
|
||||
done
|
||||
|
||||
# The ARM64 leg queues on a scarcer runner pool, so a pull request runs it
|
||||
# only when it touches what the leg verifies; a push to studio always does.
|
||||
scope:
|
||||
name: package matrix
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.pick.outputs.matrix }}
|
||||
arm64: ${{ steps.pick.outputs.arm64 }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- id: pick
|
||||
env:
|
||||
EVENT: ${{ github.event_name }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
run: |
|
||||
arm64=true
|
||||
if [ "$EVENT" = pull_request ]; then
|
||||
arm64=false
|
||||
if git diff --name-only "$BASE_SHA" HEAD |
|
||||
grep -qE '^(desktop/electron/|desktop/cmd/studio-manifest/|desktop/go\.(mod|sum)$|cmd/reasonix-studio-host/|internal/platform/(update|appupdate)/|go\.(mod|sum)$|\.github/workflows/studio\.yml$)'; then
|
||||
arm64=true
|
||||
fi
|
||||
fi
|
||||
legs='[
|
||||
{"os":"macos-latest","arch":"arm64","platform":"darwin"},
|
||||
{"os":"ubuntu-22.04","arch":"amd64","platform":"linux"},
|
||||
{"os":"windows-latest","arch":"amd64","platform":"windows","ebarch":"x64"}
|
||||
]'
|
||||
if [ "$arm64" = true ]; then
|
||||
legs="$(jq -c '. + [{"os":"windows-11-arm","arch":"arm64","platform":"windows","ebarch":"arm64"}]' <<<"$legs")"
|
||||
else
|
||||
legs="$(jq -c . <<<"$legs")"
|
||||
fi
|
||||
echo "arm64=$arm64" >> "$GITHUB_OUTPUT"
|
||||
echo "matrix={\"include\":$legs}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# The packaging contract, on real artifacts rather than on the config that
|
||||
# describes them. Unsigned on purpose: signing and notarization are a
|
||||
# distribution concern and belong to the release workflow, and treating their
|
||||
@@ -128,20 +167,12 @@ jobs:
|
||||
# cross-compile while only Windows had ever produced a package.
|
||||
package:
|
||||
name: package (${{ matrix.os }})
|
||||
needs: scope
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-latest
|
||||
arch: arm64
|
||||
platform: darwin
|
||||
- os: ubuntu-22.04
|
||||
arch: amd64
|
||||
platform: linux
|
||||
- os: windows-latest
|
||||
arch: amd64
|
||||
platform: windows
|
||||
matrix: ${{ fromJSON(needs.scope.outputs.matrix) }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@@ -174,8 +205,13 @@ jobs:
|
||||
working-directory: desktop/frontend-next
|
||||
run: pnpm install --frozen-lockfile && pnpm build
|
||||
|
||||
# The Windows legs name their architecture instead of taking the
|
||||
# toolchain's, so a runner whose Go or Node is the other architecture
|
||||
# fails here rather than shipping a mislabelled package.
|
||||
- name: Build the kernel the shell speaks to
|
||||
working-directory: desktop/electron
|
||||
env:
|
||||
REASONIX_GOARCH: ${{ matrix.platform == 'windows' && matrix.arch || '' }}
|
||||
run: pnpm install --frozen-lockfile && pnpm run build:host
|
||||
|
||||
# The .deb's absolute-path payload. A no-op off Linux, so it is not
|
||||
@@ -191,7 +227,8 @@ jobs:
|
||||
working-directory: desktop/electron
|
||||
env:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||
run: pnpm run build:dist
|
||||
ELECTRON_BUILDER_7Z_FILTER: ${{ matrix.platform == 'windows' && matrix.arch == 'arm64' && 'BCJ' || '' }}
|
||||
run: pnpm run build:dist ${{ matrix.ebarch && format('--{0}', matrix.ebarch) || '' }}
|
||||
|
||||
# The release vocabulary is GOARCH. electron-builder's own is not, and the
|
||||
# rename that reconciles them runs here rather than in the manifest.
|
||||
@@ -207,6 +244,78 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# An emulated x64 program runs on ARM64 Windows, so a package that merely
|
||||
# starts proves nothing about its architecture: every PE image in the
|
||||
# bundle has to be built for the one the package is named for.
|
||||
- name: Every PE image is built for the package's architecture
|
||||
if: matrix.platform == 'windows'
|
||||
run: |
|
||||
dir=$(find dist -maxdepth 1 -type d -name 'win*-unpacked' | head -1)
|
||||
test -n "$dir"
|
||||
node desktop/electron/packaging/pe.js "$dir" "${{ matrix.arch }}"
|
||||
|
||||
# Run on the machine the package is for: the shell's own binary as a
|
||||
# node, the kernel's identity flag, then the installer end to end, per-user
|
||||
# and silent, into a folder of its own.
|
||||
- name: The package starts, installs and uninstalls
|
||||
if: matrix.platform == 'windows'
|
||||
shell: pwsh
|
||||
env:
|
||||
ARCH: ${{ matrix.arch }}
|
||||
REASONIX_HOME: ${{ runner.temp }}/reasonix-home
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$unpacked = Get-ChildItem dist -Directory -Filter 'win*-unpacked' | Select-Object -First 1
|
||||
# A windowed executable returns to the shell at once with no stdout, so
|
||||
# it is started and waited for with its output redirected to a file.
|
||||
$env:ELECTRON_RUN_AS_NODE = '1'
|
||||
$out = Join-Path $env:RUNNER_TEMP 'shell-arch.txt'
|
||||
$node = Start-Process -FilePath "$($unpacked.FullName)/Reasonix Studio.exe" -ArgumentList '-p', 'process.arch' -RedirectStandardOutput $out -Wait -PassThru
|
||||
Remove-Item Env:ELECTRON_RUN_AS_NODE
|
||||
$arch = (Get-Content -Raw $out).Trim()
|
||||
$want = if ($env:ARCH -eq 'amd64') { 'x64' } else { $env:ARCH }
|
||||
if ($node.ExitCode -ne 0 -or $arch -ne $want) { throw "the shell reports arch '$arch' (exit $($node.ExitCode)), want $want" }
|
||||
& "$($unpacked.FullName)/resources/bin/reasonix-studio-host.exe" -instance-id
|
||||
if ($LASTEXITCODE -ne 0) { throw "the kernel exited $LASTEXITCODE" }
|
||||
|
||||
$installer = (Get-ChildItem dist -Filter "ReasonixStudio-windows-$env:ARCH-installer.exe").FullName
|
||||
$target = Join-Path $env:RUNNER_TEMP 'rx-install'
|
||||
$run = Start-Process -FilePath $installer -ArgumentList '/S', "/D=$target" -Wait -PassThru
|
||||
if ($run.ExitCode -ne 0) { throw "the installer exited $($run.ExitCode)" }
|
||||
$clock = [Diagnostics.Stopwatch]::StartNew()
|
||||
$name = [IO.Path]::GetFileNameWithoutExtension($installer)
|
||||
while (-not (Test-Path "$target/Reasonix Studio.exe") -and $clock.Elapsed.TotalSeconds -lt 120) { Start-Sleep -Seconds 2 }
|
||||
Write-Host "application appeared after $([int]$clock.Elapsed.TotalSeconds)s past the installer's exit; installer processes still alive: $(@(Get-Process -Name $name -ErrorAction SilentlyContinue).Count)"
|
||||
if (-not (Test-Path "$target/Reasonix Studio.exe")) {
|
||||
Write-Host "target holds: $((Get-ChildItem -Force $target -ErrorAction SilentlyContinue | ForEach-Object Name) -join ', ')"
|
||||
Write-Host "files under the target: $((Get-ChildItem -Recurse -File $target -ErrorAction SilentlyContinue).Count); resources holds: $((Get-ChildItem -Force "$target/resources" -ErrorAction SilentlyContinue | ForEach-Object Name) -join ', ')"
|
||||
Write-Host "Defender detections: $((Get-MpThreatDetection -ErrorAction SilentlyContinue | ForEach-Object { $_.Resources }) -join '; ')"
|
||||
Write-Host "real-time protection: $((Get-MpComputerStatus -ErrorAction SilentlyContinue).RealTimeProtectionEnabled)"
|
||||
Write-Host "PE files present: $((Get-ChildItem -Recurse -File $target -Include *.exe,*.dll,*.node | ForEach-Object { $_.FullName.Substring($target.Length) }) -join ', ')"
|
||||
Write-Host "Defender events: $((Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Windows Defender/Operational'; Id=1116,1117,1121,1122} -MaxEvents 5 -ErrorAction SilentlyContinue | ForEach-Object { $_.Message -replace '\s+',' ' }) -join ' || ')"
|
||||
Set-MpPreference -DisableRealtimeMonitoring $true
|
||||
$again = Join-Path $env:RUNNER_TEMP 'rx-install-nodefender'
|
||||
Start-Process -FilePath $installer -ArgumentList '/S', "/D=$again" -Wait | Out-Null
|
||||
Start-Sleep -Seconds 20
|
||||
Write-Host "without real-time scanning the application is present: $(Test-Path "$again/Reasonix Studio.exe")"
|
||||
throw 'the installer reported success and installed nothing'
|
||||
}
|
||||
node desktop/electron/packaging/pe.js $target $env:ARCH
|
||||
if ($LASTEXITCODE -ne 0) { throw 'the installed tree is not the package architecture' }
|
||||
|
||||
$run = Start-Process -FilePath "$target/Uninstall Reasonix Studio.exe" -ArgumentList '/S', "_?=$target" -Wait -PassThru
|
||||
if ($run.ExitCode -ne 0) { throw "the uninstaller exited $($run.ExitCode)" }
|
||||
if (Test-Path "$target/Reasonix Studio.exe") { throw 'the uninstaller left the application behind' }
|
||||
|
||||
- name: Keep the arm64 installer for the x64 refusal check
|
||||
if: matrix.platform == 'windows' && matrix.arch == 'arm64'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: studio-arm64-installer-ci
|
||||
path: dist/ReasonixStudio-windows-arm64-installer.exe
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
# The update channel is the signed manifest. A second answer to which
|
||||
# version is current is not a convenience. The .blockmap is the same
|
||||
# channel's differential index and carries the release prefix, so it also
|
||||
@@ -301,3 +410,27 @@ jobs:
|
||||
echo "::error::the manifest has no entry for ${{ matrix.platform }}-${{ matrix.arch }}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The arm64 installer carries one application and no other. Run anywhere but
|
||||
# ARM64 Windows it has to say so and stop, instead of exiting 0 with nothing
|
||||
# installed.
|
||||
arm64-installer-refuses-x64:
|
||||
name: arm64 installer refuses an x64 machine
|
||||
needs: [scope, package]
|
||||
if: needs.scope.outputs.arm64 == 'true'
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: studio-arm64-installer-ci
|
||||
path: installer
|
||||
|
||||
- name: Run it silently
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$target = Join-Path $env:RUNNER_TEMP 'rx-refused'
|
||||
$run = Start-Process -FilePath installer/ReasonixStudio-windows-arm64-installer.exe -ArgumentList '/S', "/D=$target" -Wait -PassThru
|
||||
Write-Host "exit code $($run.ExitCode)"
|
||||
if ($run.ExitCode -eq 0) { throw 'the arm64 installer exited 0 on an x64 machine' }
|
||||
if (@(Get-ChildItem -Recurse -Force $target -ErrorAction SilentlyContinue).Count -ne 0) { throw 'the arm64 installer wrote into the target on an x64 machine' }
|
||||
@@ -99,6 +99,17 @@
|
||||
# full of someone's work gets named in the first place. Refuse with an exit
|
||||
# code, because that is all a script driving this can read.
|
||||
!macro customInit
|
||||
# The arm64 installer carries only an arm64 application: on any other
|
||||
# machine electron-builder's extraction picks no archive and installs nothing
|
||||
# while reporting success.
|
||||
!ifdef APP_ARM64
|
||||
${IfNot} ${IsNativeARM64}
|
||||
DetailPrint "This installer is for Windows on ARM64 and cannot run on this machine"
|
||||
MessageBox MB_ICONSTOP "This Reasonix Studio installer is built for Windows on ARM64.$\r$\n$\r$\nDownload the x64 installer for this machine." /SD IDOK
|
||||
SetErrorLevel 3
|
||||
Abort
|
||||
${EndIf}
|
||||
!endif
|
||||
Push $R9
|
||||
!insertmacro instDirTakesNothingElse $R9
|
||||
${If} $R9 == 0
|
||||
|
||||
@@ -45,22 +45,35 @@ function go(args, env) {
|
||||
// statically so the helper needs no redistributable on the person's machine.
|
||||
const VS_INSTALLER = path.join(process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)", "Microsoft Visual Studio", "Installer");
|
||||
|
||||
function msvcVars() {
|
||||
// The helper is built for the architecture the kernel is, with that
|
||||
// architecture's own tools: the x64 toolset on an x64 machine, the ARM64 one on
|
||||
// an ARM64 machine (vcvarsarm64 is the native environment there).
|
||||
const WINDOWS_TOOLCHAIN = {
|
||||
amd64: { component: "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", vars: "vcvars64.bat", label: "x64" },
|
||||
arm64: { component: "Microsoft.VisualStudio.Component.VC.Tools.ARM64", vars: "vcvarsarm64.bat", label: "ARM64" },
|
||||
};
|
||||
|
||||
function msvcVars(toolchain) {
|
||||
const vswhere = path.join(VS_INSTALLER, "vswhere.exe");
|
||||
if (!fs.existsSync(vswhere)) return "";
|
||||
const found = spawnSync(vswhere, ["-latest", "-products", "*", "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", "-property", "installationPath"], { encoding: "utf8" });
|
||||
const found = spawnSync(vswhere, ["-latest", "-products", "*", "-requires", toolchain.component, "-property", "installationPath"], { encoding: "utf8" });
|
||||
const root = (found.stdout || "").trim().split(/\r?\n/)[0];
|
||||
const vars = root ? path.join(root, "VC", "Auxiliary", "Build", "vcvars64.bat") : "";
|
||||
const vars = root ? path.join(root, "VC", "Auxiliary", "Build", toolchain.vars) : "";
|
||||
return vars && fs.existsSync(vars) ? vars : "";
|
||||
}
|
||||
|
||||
function buildWindowsHelper() {
|
||||
const vars = msvcVars();
|
||||
function buildWindowsHelper(goarch) {
|
||||
const toolchain = WINDOWS_TOOLCHAIN[goarch];
|
||||
if (!toolchain) {
|
||||
console.error(`no MSVC toolchain is declared for windows/${goarch}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const vars = msvcVars(toolchain);
|
||||
if (!vars) {
|
||||
// A release without the helper would ship computer use switched off with
|
||||
// nothing saying why, so CI refuses; a dev build only loses the feature.
|
||||
if (process.env.CI) {
|
||||
console.error("MSVC with the x64 C++ tools is required to build reasonix-computer-helper.exe");
|
||||
console.error(`MSVC with the ${toolchain.label} C++ tools is required to build reasonix-computer-helper.exe`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.warn("MSVC not found: building without reasonix-computer-helper.exe, so computer use is unavailable in this build");
|
||||
@@ -91,9 +104,12 @@ function buildWindowsHelper() {
|
||||
if (process.platform !== "darwin") {
|
||||
// go build names the binary after the package and adds .exe where it belongs.
|
||||
// Without cgo, because with it the Linux kernel inherits the runner's glibc.
|
||||
go(["build", "-ldflags", LDFLAGS, "-o", OUT + path.sep, PKG], { CGO_ENABLED: "0" });
|
||||
// REASONIX_GOARCH names the architecture to build for; unset, Go's own is
|
||||
// used, which is the runner's.
|
||||
const goarch = (process.env.REASONIX_GOARCH || "").trim();
|
||||
go(["build", "-ldflags", LDFLAGS, "-o", OUT + path.sep, PKG], goarch ? { CGO_ENABLED: "0", GOARCH: goarch } : { CGO_ENABLED: "0" });
|
||||
console.log("built reasonix-studio-host");
|
||||
if (process.platform === "win32") buildWindowsHelper();
|
||||
if (process.platform === "win32") buildWindowsHelper(goarch || (process.arch === "arm64" ? "arm64" : "amd64"));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"use strict";
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const MACHINE = { amd64: 0x8664, arm64: 0xaa64 };
|
||||
// Written by the NSIS installer rather than built by us, and 32-bit on every
|
||||
// architecture; the release pins it by content (windows-signing-lib.ps1).
|
||||
const INSTALLER_OWNED = { "resources/elevate.exe": 0x14c, "Uninstall Reasonix Studio.exe": 0x14c };
|
||||
const EXTENSIONS = new Set([".exe", ".dll", ".node"]);
|
||||
|
||||
// The machine field of a PE image's COFF header. An emulated x64 binary and a
|
||||
// native one are both valid programs on ARM64 Windows, so the architecture a
|
||||
// package claims has to be read from the files themselves.
|
||||
function peMachine(bytes) {
|
||||
if (bytes.length < 0x40 || bytes.readUInt16LE(0) !== 0x5a4d) throw new Error("not a PE image");
|
||||
const header = bytes.readUInt32LE(0x3c);
|
||||
if (header + 6 > bytes.length || bytes.readUInt32LE(header) !== 0x00004550) throw new Error("no PE signature");
|
||||
return bytes.readUInt16LE(header + 4);
|
||||
}
|
||||
|
||||
function isPeCandidate(file) {
|
||||
if (EXTENSIONS.has(path.extname(file).toLowerCase())) return true;
|
||||
const fd = fs.openSync(file, "r");
|
||||
try {
|
||||
const head = Buffer.alloc(2);
|
||||
return fs.readSync(fd, head, 0, 2, 0) === 2 && head.readUInt16LE(0) === 0x5a4d;
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
}
|
||||
|
||||
function walk(dir, into) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) walk(full, into);
|
||||
else if (entry.isFile() && isPeCandidate(full)) into.push(full);
|
||||
}
|
||||
return into;
|
||||
}
|
||||
|
||||
// Every PE image under root with the machine it was built for.
|
||||
function machines(root) {
|
||||
return walk(root, []).sort().map((file) => {
|
||||
const name = path.relative(root, file).split(path.sep).join("/");
|
||||
try {
|
||||
return { file: name, machine: peMachine(fs.readFileSync(file)) };
|
||||
} catch (err) {
|
||||
return { file: name, machine: null, error: err.message };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// The images under root that are not built for arch; none means the tree is
|
||||
// uniformly that architecture. An empty tree is refused, not passed.
|
||||
function foreign(root, arch) {
|
||||
const want = MACHINE[arch];
|
||||
if (want === undefined) throw new Error(`unknown architecture ${arch}`);
|
||||
const found = machines(root);
|
||||
if (found.length === 0) throw new Error(`no PE images under ${root}`);
|
||||
return found.filter((f) => f.machine !== (INSTALLER_OWNED[f.file] ?? want));
|
||||
}
|
||||
|
||||
module.exports = { peMachine, machines, foreign, MACHINE };
|
||||
|
||||
if (require.main === module) {
|
||||
const [root, arch] = process.argv.slice(2);
|
||||
if (!root || !arch) {
|
||||
console.error("usage: node packaging/pe.js <dir> <amd64|arm64>");
|
||||
process.exit(2);
|
||||
}
|
||||
for (const f of machines(root)) console.log(`${f.machine === null ? f.error : "0x" + f.machine.toString(16)} ${f.file}`);
|
||||
const wrong = foreign(root, arch);
|
||||
for (const f of wrong) {
|
||||
console.error(f.error ? `::error::${f.file} cannot be read as a PE image: ${f.error}` : `::error::${f.file} is machine 0x${f.machine.toString(16)}, not ${arch}`);
|
||||
}
|
||||
process.exit(wrong.length === 0 ? 0 : 1);
|
||||
}
|
||||
@@ -1396,3 +1396,48 @@ test("the tray has a file for every Windows scale, at exactly 16 * scale pixels"
|
||||
assert.equal(trayAsset(5).pixels, 48);
|
||||
assert.equal(trayAsset(NaN).pixels, 16);
|
||||
});
|
||||
|
||||
function pe(machine) {
|
||||
const b = Buffer.alloc(0x100);
|
||||
b.writeUInt16LE(0x5a4d, 0);
|
||||
b.writeUInt32LE(0x80, 0x3c);
|
||||
b.writeUInt32LE(0x00004550, 0x80);
|
||||
b.writeUInt16LE(machine, 0x84);
|
||||
return b;
|
||||
}
|
||||
|
||||
test("the PE check reads each image's own machine and refuses a tree of another architecture", () => {
|
||||
const { peMachine, machines, foreign } = require("../packaging/pe.js");
|
||||
assert.equal(peMachine(pe(0xaa64)), 0xaa64);
|
||||
assert.throws(() => peMachine(Buffer.from("#!/bin/sh\n".repeat(10))), /not a PE/);
|
||||
assert.throws(() => peMachine(pe(0x8664).subarray(0, 0x70)), /no PE signature/);
|
||||
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "reasonix-pe-"));
|
||||
try {
|
||||
assert.throws(() => foreign(dir, "arm64"), /no PE images/);
|
||||
fs.mkdirSync(path.join(dir, "resources", "bin"), { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, "app.exe"), pe(0xaa64));
|
||||
fs.writeFileSync(path.join(dir, "resources", "bin", "host.exe"), pe(0xaa64));
|
||||
fs.writeFileSync(path.join(dir, "locales.pak"), "not an image");
|
||||
fs.writeFileSync(path.join(dir, "noext"), pe(0xaa64));
|
||||
assert.equal(machines(dir).length, 3);
|
||||
assert.deepEqual(foreign(dir, "arm64"), []);
|
||||
assert.deepEqual(foreign(dir, "amd64").map((f) => f.file), ["app.exe", "noext", "resources/bin/host.exe"]);
|
||||
fs.writeFileSync(path.join(dir, "ffmpeg.dll"), pe(0x8664));
|
||||
assert.deepEqual(foreign(dir, "arm64").map((f) => f.file), ["ffmpeg.dll"]);
|
||||
assert.throws(() => foreign(dir, "x64"), /unknown architecture/);
|
||||
fs.writeFileSync(path.join(dir, "broken.dll"), "text, not an image");
|
||||
const broken = foreign(dir, "arm64").find((f) => f.file === "broken.dll");
|
||||
assert.match(broken.error, /not a PE/);
|
||||
fs.rmSync(path.join(dir, "broken.dll"));
|
||||
fs.writeFileSync(path.join(dir, "ffmpeg.dll"), pe(0xaa64));
|
||||
fs.writeFileSync(path.join(dir, "resources", "elevate.exe"), pe(0x14c));
|
||||
assert.deepEqual(foreign(dir, "arm64"), []);
|
||||
fs.writeFileSync(path.join(dir, "Uninstall Reasonix Studio.exe"), pe(0x14c));
|
||||
assert.deepEqual(foreign(dir, "arm64"), []);
|
||||
fs.writeFileSync(path.join(dir, "resources", "elevate.exe"), pe(0xaa64));
|
||||
assert.deepEqual(foreign(dir, "arm64").map((f) => f.file), ["resources/elevate.exe"]);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user