diff --git a/src/dotnet.ts b/src/dotnet.ts index f94ae61..7da230e 100644 --- a/src/dotnet.ts +++ b/src/dotnet.ts @@ -1,6 +1,25 @@ import { CrossSpawnArgs } from "@malept/cross-spawn-promise"; import { CrossSpawnExeOptions, spawnWrapperFromFunction } from "./wrapper"; +/** + * Installation instructions for dependencies related to running .NET executables on the + * host platform (i.e., Mono on non-Windows platforms). + */ +export function dotNetDependencyInstallInstructions(): string { + switch (process.platform) { + /* istanbul ignore next */ + case "win32": + return "No wrapper necessary"; + case "darwin": + return "Run `brew install mono` to install Mono on macOS via Homebrew."; + case "linux": + return "Consult your Linux distribution's package manager to determine how to install Mono."; + /* istanbul ignore next */ + default: + return "Consult your operating system's package manager to determine how to install Mono."; + } +} + /** * Heuristically determine the path to `mono` to use. * @@ -31,5 +50,7 @@ export async function spawnDotNet( args?: CrossSpawnArgs, options?: CrossSpawnExeOptions ): Promise { + options ??= {}; + options.wrapperInstructions ??= dotNetDependencyInstallInstructions(); return spawnWrapperFromFunction(determineDotNetWrapper, cmd, args, options); } diff --git a/src/exe.ts b/src/exe.ts index 41e5db7..027007c 100644 --- a/src/exe.ts +++ b/src/exe.ts @@ -2,7 +2,11 @@ import { CrossSpawnArgs } from "@malept/cross-spawn-promise"; import { CrossSpawnExeOptions, spawnWrapperFromFunction } from "./wrapper"; import { is64BitArch } from "./arch"; -function installInstructions(): string { +/** + * Installation instructions for dependencies related to running Windows executables on the + * host platform (i.e., Wine on non-Windows platforms). + */ +export function exeDependencyInstallInstructions(): string { switch (process.platform) { /* istanbul ignore next */ case "win32": @@ -53,11 +57,7 @@ export async function spawnExe( args?: CrossSpawnArgs, options?: CrossSpawnExeOptions ): Promise { - if (!options?.wrapperInstructions) { - if (!options) { - options = {}; - } - options.wrapperInstructions = installInstructions(); - } + options ??= {}; + options.wrapperInstructions ??= exeDependencyInstallInstructions(); return spawnWrapperFromFunction(determineWineWrapper, cmd, args, options); } diff --git a/src/index.ts b/src/index.ts index 1e06170..73393d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,10 +4,11 @@ export { DetermineWrapperFunction, spawnWrapper as spawn, spawnWrapperFromFunction, + WrapperError, } from "./wrapper"; export { is64BitArch } from "./arch"; export { normalizePath } from "./normalize-path"; -export { spawnDotNet } from "./dotnet"; -export { spawnExe } from "./exe"; +export { dotNetDependencyInstallInstructions, spawnDotNet } from "./dotnet"; +export { exeDependencyInstallInstructions, spawnExe } from "./exe"; diff --git a/src/wrapper.ts b/src/wrapper.ts index 0c49977..99f2f5b 100644 --- a/src/wrapper.ts +++ b/src/wrapper.ts @@ -82,9 +82,7 @@ export async function spawnWrapper( args?: CrossSpawnArgs, options?: CrossSpawnExeOptions ): Promise { - if (!options) { - options = {} as CrossSpawnExeOptions; - } + options ??= {} as CrossSpawnExeOptions; const { wrapperCommand, wrapperInstructions, ...crossSpawnOptions } = options; if (wrapperCommand) { diff --git a/test/dotnet.ts b/test/dotnet.ts index 0f096ed..343e42a 100644 --- a/test/dotnet.ts +++ b/test/dotnet.ts @@ -1,6 +1,11 @@ import * as path from "path"; -import { determineDotNetWrapper, spawnDotNet } from "../src/dotnet"; +import { + determineDotNetWrapper, + dotNetDependencyInstallInstructions, + spawnDotNet, +} from "../src/dotnet"; import { normalizePath } from "../src/normalize-path"; +import { canRunWindowsExeNatively, WrapperError } from "../src/wrapper"; import test from "ava"; const fixturePath = @@ -49,7 +54,7 @@ test.serial("runs a dotnet binary with arguments", async (t) => { ); }); -test.serial("runs a Windows binary with a filename argument", async (t) => { +test.serial("runs a dotnet binary with a filename argument", async (t) => { t.is(process.env.MONO_BINARY, undefined); const output = await spawnDotNet(path.join(fixturePath, "hello.dotnet.exe"), [ await normalizePath(path.join(fixturePath, "input.txt")), @@ -59,3 +64,37 @@ test.serial("runs a Windows binary with a filename argument", async (t) => { "Hello DotNet World, arguments passed\nInput\nFile" ); }); + +if (!canRunWindowsExeNatively()) { + test.serial( + "fails to run a dotnet binary with the default wrapper instructions", + async (t) => { + process.env.MONO_BINARY = "mono-nonexistent"; + await t.throwsAsync( + async () => spawnDotNet(path.join(fixturePath, "hello.dotnet.exe")), + { + instanceOf: WrapperError, + message: `Wrapper command 'mono-nonexistent' not found on the system. ${dotNetDependencyInstallInstructions()}`, + } + ); + } + ); + + test.serial( + "fails to run a dotnet binary with custom wrapper instructions", + async (t) => { + process.env.MONO_BINARY = "mono-nonexistent"; + await t.throwsAsync( + async () => + spawnDotNet(path.join(fixturePath, "hello.dotnet.exe"), [], { + wrapperInstructions: "Custom text.", + }), + { + instanceOf: WrapperError, + message: + "Wrapper command 'mono-nonexistent' not found on the system. Custom text.", + } + ); + } + ); +} diff --git a/test/exe.ts b/test/exe.ts index 2d1a637..f11f60f 100644 --- a/test/exe.ts +++ b/test/exe.ts @@ -1,5 +1,9 @@ -import { canRunWindowsExeNatively } from "../src/wrapper"; -import { determineWineWrapper, spawnExe } from "../src/exe"; +import { canRunWindowsExeNatively, WrapperError } from "../src/wrapper"; +import { + determineWineWrapper, + exeDependencyInstallInstructions, + spawnExe, +} from "../src/exe"; import { normalizePath } from "../src/normalize-path"; import * as path from "path"; import * as sinon from "sinon"; @@ -83,19 +87,53 @@ test.serial("runs a Windows binary with a filename argument", async (t) => { ); }); -test.serial( - "runs a Windows binary with a filename argument containing a space", - async (t) => { - t.is(process.env.WINE_BINARY, undefined); - if (!canRunWindowsExeNatively()) { - t.timeout(wineTimeout, "wine is taking too long to execute"); +if (!canRunWindowsExeNatively()) { + test.serial( + "runs a Windows binary with a filename argument containing a space", + async (t) => { + t.is(process.env.WINE_BINARY, undefined); + if (!canRunWindowsExeNatively()) { + t.timeout(wineTimeout, "wine is taking too long to execute"); + } + const output = await spawnExe(path.join(fixturePath, "hello.exe"), [ + await normalizePath(path.join(fixturePath, "input with space.txt")), + ]); + t.is( + output.trim().replace(/\r/g, ""), + "Hello EXE World, arguments passed\nInput\nFile With Space" + ); } - const output = await spawnExe(path.join(fixturePath, "hello.exe"), [ - await normalizePath(path.join(fixturePath, "input with space.txt")), - ]); - t.is( - output.trim().replace(/\r/g, ""), - "Hello EXE World, arguments passed\nInput\nFile With Space" - ); - } -); + ); + + test.serial( + "fails to run a Windows binary with the default wrapper instructions", + async (t) => { + process.env.WINE_BINARY = "wine-nonexistent"; + await t.throwsAsync( + async () => spawnExe(path.join(fixturePath, "hello.exe")), + { + instanceOf: WrapperError, + message: `Wrapper command 'wine-nonexistent' not found on the system. ${exeDependencyInstallInstructions()}`, + } + ); + } + ); + + test.serial( + "fails to run a Windows binary with custom wrapper instructions", + async (t) => { + process.env.WINE_BINARY = "wine-nonexistent"; + await t.throwsAsync( + async () => + spawnExe(path.join(fixturePath, "hello.exe"), [], { + wrapperInstructions: "Custom text.", + }), + { + instanceOf: WrapperError, + message: + "Wrapper command 'wine-nonexistent' not found on the system. Custom text.", + } + ); + } + ); +}