Skip to content

Commit

Permalink
feat: add cross-compilation feature
Browse files Browse the repository at this point in the history
  • Loading branch information
adbayb committed Nov 12, 2024
1 parent e351a81 commit da5d78a
Show file tree
Hide file tree
Showing 10 changed files with 584 additions and 49 deletions.
5 changes: 5 additions & 0 deletions .changeset/spotty-cows-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"quickbundle": minor
---

Add cross-compilation feature.
16 changes: 14 additions & 2 deletions examples/multiple-standalones/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
#!/usr/bin/env node

import process from "node:process";

import ora from "ora";

console.info("A standalone program");
console.info("Hello world\n");

console.debug(
"Debug information",
JSON.stringify(
{
embeddedNodeVersion: process.version,
},
null,
2,
),
);

const spinner = ora("Fake processing").start();

Expand All @@ -12,5 +25,4 @@ const sleep = async (duration = 3000) => {

void sleep().then(() => {
spinner.stop();
console.info("End processing");
});
16 changes: 14 additions & 2 deletions examples/single-standalone/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
#!/usr/bin/env node

import process from "node:process";

import ora from "ora";

console.info("A standalone program");
console.info("Hello world\n");

console.debug(
"Debug information",
JSON.stringify(
{
embeddedNodeVersion: process.version,
},
null,
2,
),
);

const spinner = ora("Fake processing").start();

Expand All @@ -12,5 +25,4 @@ const sleep = async (duration = 3000) => {

void sleep().then(() => {
spinner.stop();
console.info("End processing");
});
329 changes: 329 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

22 changes: 20 additions & 2 deletions quickbundle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
Quickbundle allows you to bundle a library in a **quick**, **fast** and **easy** way:

- Fast build and watch mode powered by Rollup[^1] and SWC[^2].
- Compile mode to create standalone binaries for systems that do not have Node.js installed[^3].
- Compile mode to compile and cross compile standalone binaries for systems that do not have Node.js installed[^3].
- Zero configuration: define the build artifacts in your `package.json`, and you're all set!
- Support of `cjs` & `esm` module formats output.
- Support of several loaders including JavaScript, TypeScript, JSX, JSON, and Images.
Expand Down Expand Up @@ -113,7 +113,7 @@ yarn add quickbundle
"name": "lib", // Package name
"source": "./src/index.ts", // Source code entry point. Make sure that it starts with `#!/usr/bin/env node` pragme to make the binary portable for consumers who would like to use it by installing the package instead of using the generated standalone executable.
"bin": {
"your-binary-name": "./dist/index.cjs", // Binary information to get the executable name from the key and, from the value, the bundled file to generate from the source code and inject into the executable. The generated executable will be located in the same folder as the bundled file and, dependending on the current operating system running the `compile` command, the executable will be named either `your-binary-name.exe` on Windows or `your-binary-name` on Linux and macOS.
"your-binary-name": "./dist/index.cjs", // Binary information to get the executable name from the key and, from the value, the bundled file to generate from the source code and inject into the executable. The generated executable will be located in the same folder as the bundled file and, by default, dependending on the current operating system running the `compile` command, the executable will be named either `your-binary-name.exe` on Windows or `your-binary-name` on Linux and macOS.
},
// "bin": "./dist/index.cjs", // Or, if the binary name follows the package name, you can define a string-based `bin` value.
"scripts": {
Expand Down Expand Up @@ -171,6 +171,24 @@ quickbundle watch --source-maps

Enabling source map generation is needed only if a build is [obfuscated (minified)](#optimize-the-build-output) for debugging-easing purposes. It generally pairs with the [`minification` flag](#optimize-the-build-output).

### Cross compilation to other platforms

By default, the `compile` command embeds the runtime at the origin of its execution which means it generates executables compatible only with machines running the same operating system and processor architecture.

However, Quickbundle provides the ability to target a different operating system or processor architecture (also known as cross compilation):

```bash
quickbundle compile --runtime node-v23.1.0-darwin-arm64 # Embeds Node v23 runtime built for macOS ARM64 target
quickbundle compile --runtime node-v23.1.0-darwin-x64 # Embeds Node v23 runtime built for macOS X64 target
quickbundle compile --runtime node-v23.1.0-linux-arm64 # Embeds Node v23 runtime built for Linux ARM64 target
quickbundle compile --runtime node-v23.1.0-linux-x64 # Embeds Node v23 runtime built for Linux X64 target
quickbundle compile --runtime node-v23.1.0-win-arm64 # Embeds Node v23 runtime built for Windows ARM64 target
quickbundle compile --runtime node-v23.1.0-win-x64 # Embeds Node v23 runtime built for Windows X64 target
```

> [!note]
> The accepted runtime input are the one listed in [https://nodejs.org/download/release/vx.y.z](https://nodejs.org/download/release/latest/) with the following format `node-vx.y.z-(darwin|linux|win)-(arm64|x64|x86)`.
<br>

## ☑️ Roadmap
Expand Down
2 changes: 2 additions & 0 deletions quickbundle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-url": "^8.0.2",
"@swc/core": "^1.9.1",
"decompress": "^4.2.1",
"gzip-size": "^7.0.0",
"rollup": "^4.24.4",
"rollup-plugin-dts": "^6.1.1",
Expand All @@ -66,6 +67,7 @@
"termost": "^1.2.0"
},
"devDependencies": {
"@types/decompress": "4.2.7",
"@types/node": "22.9.0"
}
}
8 changes: 5 additions & 3 deletions quickbundle/src/bundler/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { join } from "node:path";
import { createRequire } from "node:module";

import { swc } from "rollup-plugin-swc3";
Expand All @@ -10,11 +9,14 @@ import { nodeResolve } from "@rollup/plugin-node-resolve";
import json from "@rollup/plugin-json";
import commonjs from "@rollup/plugin-commonjs";

import { CWD } from "../constants";
import { resolveFromExternalDirectory } from "../helpers";
import { isRecord } from "./helpers";

const require = createRequire(import.meta.url);
const PKG = require(join(CWD, "./package.json")) as PackageJson;

const PKG = require(
resolveFromExternalDirectory("package.json"),
) as PackageJson;

type PackageJson = {
name?: string;
Expand Down
123 changes: 101 additions & 22 deletions quickbundle/src/commands/compile.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,48 @@
import { basename, dirname, resolve } from "node:path";
import process from "node:process";
import { basename, dirname, join, resolve } from "node:path";
import os from "node:os";
import { rm, writeFile } from "node:fs/promises";

import { helpers } from "termost";
import type { Termost } from "termost";

import {
copyFile,
createRegExpMatcher,
download,
removePath,
resolveFromInternalDirectory,
unzip,
writeFile,
} from "../helpers";
import { createConfiguration } from "../bundler/config";
import type { Configuration } from "../bundler/config";
import { build } from "../bundler/build";

type CompileCommandContext = {
config: Configuration;
osType: "linux" | "macos" | "windows";
osType: OsType;
runtimeInput: string;
};

const TEMPORARY_PATH = resolveFromInternalDirectory("dist", "tmp");
const TEMPORARY_DOWNLOAD_PATH = join(TEMPORARY_PATH, "zip");
const TEMPORARY_RUNTIME_PATH = join(TEMPORARY_PATH, "runtime");

export const createCompileCommand = (program: Termost) => {
return program
.command<CompileCommandContext>({
name: "compile",
description: "Compiles the source code into a self-contained executable",
})
.task({
key: "osType",
label: "Get context",
handler() {
const type = os.type();

return type === "Windows_NT"
? "windows"
: type === "Darwin"
? "macos"
: "linux";
.option({
key: "runtimeInput",
name: {
long: "runtime",
short: "r",
},
description:
"Set a different runtime target than the one available in the local machine running the command.",
defaultValue: "local",
})
.task({
key: "config",
Expand All @@ -44,6 +55,51 @@ export const createCompileCommand = (program: Termost) => {
});
},
})
.task({
key: "osType",
label({ runtimeInput }) {
return `Get runtime \`${runtimeInput}\``;
},
async handler({ runtimeInput }) {
if (runtimeInput === "local") {
await copyFile(process.execPath, TEMPORARY_RUNTIME_PATH);

return getOsType(os.type());
}

const matchedRuntimeParts = matchRuntimeParts(runtimeInput);

if (!matchedRuntimeParts) {
throw new Error(
"Invalid `runtime` flag input. The accepted targets are the one listed in https://nodejs.org/download/release/ with the following format `node-vx.y.z-(darwin|linux|win)-(arm64|x64|x86)`.",
);
}

const osType = getOsType(matchedRuntimeParts.os);
const extension = osType === "windows" ? "zip" : "tar.gz";

await download(
`https://nodejs.org/download/release/${matchedRuntimeParts.version}/${runtimeInput}.${extension}`,
TEMPORARY_DOWNLOAD_PATH,
);

await unzip(
{
path: TEMPORARY_DOWNLOAD_PATH,
targetArchivePath:
osType === "windows"
? join(runtimeInput, "node.exe")
: join(runtimeInput, "bin", "node"),
},
{
directoryPath: dirname(TEMPORARY_RUNTIME_PATH),
filename: basename(TEMPORARY_RUNTIME_PATH),
},
);

return osType;
},
})
.task({
label: "Build",
async handler({ config }) {
Expand Down Expand Up @@ -75,6 +131,30 @@ export const createCompileCommand = (program: Termost) => {
});
};

type OsType = "linux" | "macos" | "windows";

const getOsType = (input: string): OsType => {
switch (input) {
case "Windows_NT":
case "win":
return "windows";
case "Darwin":
case "darwin":
return "macos";
case "Linux":
case "linux":
return "linux";
default:
throw new Error(`Unsupported operating system \`${input}\``);
}
};

const matchRuntimeParts = createRegExpMatcher<
"architecture" | "os" | "version"
>(
/^node-(?<version>v\d+\.\d+\.\d+)-(?<os>darwin|linux|win)-(?<architecture>arm64|x64|x86)$/,
);

const compile = async ({
bin,
input,
Expand Down Expand Up @@ -109,15 +189,12 @@ const compile = async ({
useCodeCache: false,
useSnapshot: false,
}),
"utf-8",
);

await Promise.all(
[
`node --experimental-sea-config ${seaConfigFileName}`,
`node -e "require('fs').copyFileSync(process.execPath, '${executableFileName}')"`,
].map(async (command) => helpers.exec(command)),
);
await Promise.all([
helpers.exec(`node --experimental-sea-config ${seaConfigFileName}`),
copyFile(TEMPORARY_RUNTIME_PATH, executableFileName),
]);

if (osType === "macos") {
await helpers.exec(`codesign --remove-signature ${executableFileName}`);
Expand All @@ -132,6 +209,8 @@ const compile = async ({
}

await Promise.all(
[blobFileName, seaConfigFileName].map(async (file) => rm(file)),
[blobFileName, seaConfigFileName, TEMPORARY_PATH].map(async (path) =>
removePath(path),
),
);
};
3 changes: 0 additions & 3 deletions quickbundle/src/constants.ts

This file was deleted.

Loading

0 comments on commit da5d78a

Please sign in to comment.