Skip to content

Commit

Permalink
feat: default install instructions for runtime dependencies (#49)
Browse files Browse the repository at this point in the history
* feat(exe): export (wine) dependency install instructions function
* refactor: use nullish coalescing operator when possible
* feat(dotnet): add exported install instructions + tests
* feat: export WrapperError for instanceof checks
  • Loading branch information
malept authored Jul 14, 2021
1 parent f680802 commit 21e97b6
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 31 deletions.
21 changes: 21 additions & 0 deletions src/dotnet.ts
Original file line number Diff line number Diff line change
@@ -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.
*
Expand Down Expand Up @@ -31,5 +50,7 @@ export async function spawnDotNet(
args?: CrossSpawnArgs,
options?: CrossSpawnExeOptions
): Promise<string> {
options ??= {};
options.wrapperInstructions ??= dotNetDependencyInstallInstructions();
return spawnWrapperFromFunction(determineDotNetWrapper, cmd, args, options);
}
14 changes: 7 additions & 7 deletions src/exe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -53,11 +57,7 @@ export async function spawnExe(
args?: CrossSpawnArgs,
options?: CrossSpawnExeOptions
): Promise<string> {
if (!options?.wrapperInstructions) {
if (!options) {
options = {};
}
options.wrapperInstructions = installInstructions();
}
options ??= {};
options.wrapperInstructions ??= exeDependencyInstallInstructions();
return spawnWrapperFromFunction(determineWineWrapper, cmd, args, options);
}
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
4 changes: 1 addition & 3 deletions src/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,7 @@ export async function spawnWrapper(
args?: CrossSpawnArgs,
options?: CrossSpawnExeOptions
): Promise<string> {
if (!options) {
options = {} as CrossSpawnExeOptions;
}
options ??= {} as CrossSpawnExeOptions;

const { wrapperCommand, wrapperInstructions, ...crossSpawnOptions } = options;
if (wrapperCommand) {
Expand Down
43 changes: 41 additions & 2 deletions test/dotnet.ts
Original file line number Diff line number Diff line change
@@ -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 =
Expand Down Expand Up @@ -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")),
Expand All @@ -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.",
}
);
}
);
}
72 changes: 55 additions & 17 deletions test/exe.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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.",
}
);
}
);
}

0 comments on commit 21e97b6

Please sign in to comment.