Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(build): add build package #12

Merged
merged 1 commit into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
example:
- changed-files:
- any-glob-to-any-file: app/example/**
core:
- changed-files:
- any-glob-to-any-file: core/**
25 changes: 25 additions & 0 deletions .github/workflows/bump.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Bump

on:
workflow_dispatch:

jobs:
bump:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true

- name: Setup
uses: denoland/setup-deno@v2

- name: Bump versions
run: deno task release --bump
env:
GITHUB_ACTOR: "${{ github.actor }}"
GITHUB_EMAIL: "${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com"
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,29 @@ jobs:

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5

build:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup
uses: denoland/setup-deno@v2

- name: Build
run: deno task compile

publish:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup
uses: denoland/setup-deno@v2

- name: Publish (dry run)
run: deno publish --dry-run
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ jobs:
uses: denoland/setup-deno@v2

- name: Release packages
run: deno task version --release
run: deno task release --release
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
1 change: 1 addition & 0 deletions .github/workflows/title.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ jobs:
async
build
cli
example
git
github
git
Expand Down
7 changes: 7 additions & 0 deletions app/example/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@roka/example",
"version": "0.0.0",
"compile": {
"main": "example.ts"
}
}
1 change: 1 addition & 0 deletions app/example/example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log("Hello, world!");
1 change: 1 addition & 0 deletions core/async/deno.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "@roka/async",
"version": "0.0.0",
"exports": {
"./pool": "./pool.ts"
}
Expand Down
197 changes: 197 additions & 0 deletions core/build/compile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { Command, EnumType } from "@cliffy/command";
import { pool } from "@roka/async/pool";
import { getWorkspace, type Package, PackageError } from "@roka/build/package";
import { displayVersion } from "@roka/build/version";
import { assert } from "@std/assert/assert";
import { encodeHex } from "@std/encoding";
import { basename, join, relative } from "@std/path";
import type { Permissions } from "./package.ts";

/** Options for compiling a package. */
export interface CompileOptions {
/**
* Target OS architectures.
* @default {[Deno.build.target]}
*/
target?: string[];
/** Use update version. */
release?: boolean;
/** Bundle artifacts. */
bundle?: boolean;
/** Create a checksum file. */
checksum?: boolean;
/** Install at given directory. */
install?: string;
/** Max concurrent compilations. */
concurrency?: number;
}

/** Return all compile targets support by `deno compile`. */
export async function compileTargets(): Promise<string[]> {
const command = new Deno.Command("deno", { args: ["compile", "--target"] });
const { code, stderr } = await command.output();
assert(code !== 0, "Expected the command to fail");
const match = new TextDecoder().decode(stderr).match(
/\[possible values: (?<targets>.+)\]/,

Check warning on line 35 in core/build/compile.ts

View check run for this annotation

Codecov / codecov/patch

core/build/compile.ts#L30-L35

Added lines #L30 - L35 were not covered by tests
);
assert(match?.groups?.targets, "Expected targets in stderr");
return match.groups.targets.split(", ");
}

Check warning on line 39 in core/build/compile.ts

View check run for this annotation

Codecov / codecov/patch

core/build/compile.ts#L37-L39

Added lines #L37 - L39 were not covered by tests

/** Compile a package using the given options. */
export async function compile(
pkg: Package,
options?: CompileOptions,

Check warning on line 44 in core/build/compile.ts

View check run for this annotation

Codecov / codecov/patch

core/build/compile.ts#L42-L44

Added lines #L42 - L44 were not covered by tests
): Promise<string[]> {
assert(pkg.config.compile, "Compile configuration is required");
const {
target = [Deno.build.target],
concurrency = navigator.hardwareConcurrency,
} = options ?? {};
const { main, include = [], kv = false, permissions = {} } =
pkg.config.compile ?? {};
assert(main, "Compile entrypoint is required");
const version = options?.release ? pkg.config?.version : pkg.version;
if (!version) {
throw new PackageError(`Cannot determine version for ${pkg.config.name}`);
}
const directory = join("dist", pkg.module, version);
try {
await Deno.remove(directory, { recursive: true });
} catch (e: unknown) {
if (!(e instanceof Deno.errors.NotFound)) throw e;
}
await Deno.mkdir(directory, { recursive: true });
const config = join(directory, "deno.json");
pkg.config.version = version;
await Deno.writeTextFile(config, JSON.stringify(pkg.config, null, 2));
const artifacts = await pool(target, async (target) => {
const output = join(directory, target, pkg.module);
const args = [
"compile",
`--target=${target}`,
permissionArgs(permissions, "read", false),
permissionArgs(permissions, "write", false),
permissionArgs(permissions, "net", false),
permissionArgs(permissions, "sys", true),
permissionArgs(permissions, "env", true),
permissionArgs(permissions, "run", true),
permissionArgs(permissions, "ffi", true),
"--no-prompt",
kv ? "--unstable-kv" : [],
`--include=${config}`,
include.map((path) => `--include=${join(pkg.directory, path)}`),
`--output=${output}`,
join(pkg.directory, main),
].flat();
const command = new Deno.Command("deno", { args });
const { code, stderr } = await command.output();
if (code !== 0) {
console.error(new TextDecoder().decode(stderr));
throw new PackageError(`Compile failed for ${pkg.config.name}`);
}
if (options?.install) {
await Deno.mkdir(options.install, { recursive: true });
await Deno.copyFile(output, join(options.install, basename(output)));
}
if (!options?.bundle) return output;
const isWindows = target.includes("windows");
const build = join(directory, target);
const bundle = `${build}.${isWindows ? "zip" : "tar.gz"}`;
await (isWindows ? zip : tar)(build, bundle);
return bundle;
}, { concurrency });
if (options?.checksum) {
const checksumFile = join(directory, "sha256.txt");
await Deno.writeTextFile(
checksumFile,
await sha256sum(directory, artifacts),

Check warning on line 108 in core/build/compile.ts

View check run for this annotation

Codecov / codecov/patch

core/build/compile.ts#L46-L108

Added lines #L46 - L108 were not covered by tests
);
artifacts.push(checksumFile);
}
return artifacts;
}

Check warning on line 113 in core/build/compile.ts

View check run for this annotation

Codecov / codecov/patch

core/build/compile.ts#L110-L113

Added lines #L110 - L113 were not covered by tests

function permissionArgs<P extends keyof Permissions>(
permissions: Permissions,
name: P,
merge: boolean,

Check warning on line 118 in core/build/compile.ts

View check run for this annotation

Codecov / codecov/patch

core/build/compile.ts#L115-L118

Added lines #L115 - L118 were not covered by tests
): string[] {
const value = permissions[name];
if (value === undefined) return [];
if (typeof value === "boolean") {
return value ? [`--allow-${name}`] : [`--no-allow-${name}`];
}
const values = Array.isArray(value) ? value : [value];
if (merge) return [`--allow-${name}=${values.join(",")}`];
return values.map((v) => `--allow-${name}=${v}`);
}

Check warning on line 128 in core/build/compile.ts

View check run for this annotation

Codecov / codecov/patch

core/build/compile.ts#L120-L128

Added lines #L120 - L128 were not covered by tests

async function tar(directory: string, output: string) {
const command = new Deno.Command("tar", {
args: ["-czf", output, "-C", directory, "."],
});
const { code, stderr } = await command.output();
if (code !== 0) {
console.error(new TextDecoder().decode(stderr));
throw new PackageError(`Bundle failed for ${output}`);
}
}

Check warning on line 139 in core/build/compile.ts

View check run for this annotation

Codecov / codecov/patch

core/build/compile.ts#L130-L139

Added lines #L130 - L139 were not covered by tests

async function zip(directory: string, output: string) {
const command = new Deno.Command("zip", {
cwd: directory,
args: ["-r", relative(directory, output), "."],
});
const { code, stderr } = await command.output();
if (code !== 0) {
console.error(new TextDecoder().decode(stderr));
throw new PackageError(`Bundle failed for ${output}`);
}
}

Check warning on line 151 in core/build/compile.ts

View check run for this annotation

Codecov / codecov/patch

core/build/compile.ts#L141-L151

Added lines #L141 - L151 were not covered by tests

async function sha256sum(directory: string, artifacts: string[]) {
const checksums = await Promise.all(artifacts.map(async (artifact) => {
const content = await Deno.readFile(artifact);
const checksum = encodeHex(await crypto.subtle.digest("SHA-256", content));
return `${checksum} ${relative(directory, artifact)}\n`;
}));
return checksums.join("");
}

Check warning on line 160 in core/build/compile.ts

View check run for this annotation

Codecov / codecov/patch

core/build/compile.ts#L153-L160

Added lines #L153 - L160 were not covered by tests

async function main(args: string[]) {
const command = new Command()
.name("compile")
.description("Compile packages.")
.version(await displayVersion())
.arguments("[directories...:file]")
.type("target", new EnumType(await compileTargets()))
.option("--target=<architechture:target>", "Target OS architecture.", {
collect: true,
})
.option("--release", "Use new release version.", { default: false })
.option("--bundle", "Zip and bundle artfifacts.", { default: false })
.option("--checksum", "Create a checksum file.", { default: false })
.option("--install=<directory:file>", "Install at given directory.")
.option("--concurrency=<number:number>", "Max concurrent compilations.")
.action(
async (options, ...directories) => {
if (directories.length === 0) directories = ["."];
const packages = await getWorkspace({ directories });
await pool(
packages.filter((pkg) => pkg.config.compile),
async (pkg) => {
const artifacts = await compile(pkg, options);
console.log(`🏺 Compiled ${pkg.module}`);
for (const artifact of artifacts) {
console.log(` ${artifact}`);
}
},
options,

Check warning on line 190 in core/build/compile.ts

View check run for this annotation

Codecov / codecov/patch

core/build/compile.ts#L162-L190

Added lines #L162 - L190 were not covered by tests
);
},

Check warning on line 192 in core/build/compile.ts

View check run for this annotation

Codecov / codecov/patch

core/build/compile.ts#L192

Added line #L192 was not covered by tests
);
await command.parse(args);
}

Check warning on line 195 in core/build/compile.ts

View check run for this annotation

Codecov / codecov/patch

core/build/compile.ts#L194-L195

Added lines #L194 - L195 were not covered by tests

if (import.meta.main) await main(Deno.args);
10 changes: 10 additions & 0 deletions core/build/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "@roka/build",
"version": "0.0.0",
"exports": {
"./compile": "./compile.ts",
"./package": "./package.ts",
"./release": "./release.ts",
"./version": "./version.ts"
}
}
Loading
Loading