Skip to content

Commit

Permalink
feat(cli/bunx): support --no-install flag (#16315)
Browse files Browse the repository at this point in the history
  • Loading branch information
DonIsaac authored and cirospaciari committed Jan 13, 2025
1 parent 5104220 commit 258aee7
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 10 deletions.
1 change: 1 addition & 0 deletions src/bun.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1613,6 +1613,7 @@ pub const renamer = @import("./renamer.zig");
// TODO: Rename to SourceMap as this is a struct.
pub const sourcemap = @import("./sourcemap/sourcemap.zig");

/// Attempt to coerce some value into a byte slice.
pub fn asByteSlice(buffer: anytype) []const u8 {
return switch (@TypeOf(buffer)) {
[]const u8, []u8, [:0]const u8, [:0]u8 => buffer.ptr[0..buffer.len],
Expand Down
48 changes: 41 additions & 7 deletions src/cli/bunx_command.zig
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const std = @import("std");
const bun = @import("root").bun;
const string = bun.string;
const Allocator = std.mem.Allocator;
const Output = bun.Output;
const Global = bun.Global;
const Environment = bun.Environment;
Expand All @@ -8,11 +10,11 @@ const MutableString = bun.MutableString;
const stringZ = bun.stringZ;
const default_allocator = bun.default_allocator;
const C = bun.C;
const std = @import("std");
const cli = @import("../cli.zig");

const Command = cli.Command;
const Run = @import("./run_command.zig").RunCommand;
const Allocator = std.mem.Allocator;
const UpdateRequest = bun.PackageManager.UpdateRequest;

const debug = Output.scoped(.bunx, false);

Expand All @@ -30,6 +32,9 @@ pub const BunxCommand = struct {
// purposes.
verbose_install: bool = false,
silent_install: bool = false,
/// Skip installing the package, only running the target command if its
/// already downloaded. If its not, `bunx` exits with an error.
no_install: bool = false,
allocator: Allocator,

/// Create a new `Options` instance by parsing CLI arguments. `ctx` may be mutated.
Expand Down Expand Up @@ -65,6 +70,8 @@ pub const BunxCommand = struct {
opts.silent_install = true;
} else if (strings.eqlComptime(positional, "--bun") or strings.eqlComptime(positional, "-b")) {
ctx.debug.run_in_bun = true;
} else if (strings.eqlComptime(positional, "--no-install")) {
opts.no_install = true;
}
} else {
if (!found_subcommand_name) {
Expand Down Expand Up @@ -298,9 +305,9 @@ pub const BunxCommand = struct {
var opts = try Options.parse(ctx, argv);
defer opts.deinit();

var requests_buf = bun.PackageManager.UpdateRequest.Array.initCapacity(ctx.allocator, 64) catch bun.outOfMemory();
var requests_buf = UpdateRequest.Array.initCapacity(ctx.allocator, 64) catch bun.outOfMemory();
defer requests_buf.deinit(ctx.allocator);
const update_requests = bun.PackageManager.UpdateRequest.parse(
const update_requests = UpdateRequest.parse(
ctx.allocator,
null,
ctx.log,
Expand Down Expand Up @@ -335,6 +342,7 @@ pub const BunxCommand = struct {

// fast path: they're actually using this interchangeably with `bun run`
// so we use Bun.which to check
// SAFETY: initialized by Run.configureEnvForRun
var this_transpiler: bun.Transpiler = undefined;
var ORIGINAL_PATH: string = "";

Expand Down Expand Up @@ -384,7 +392,7 @@ pub const BunxCommand = struct {
else => ":",
};

const has_banned_char = bun.strings.indexAnyComptime(update_request.name, banned_path_chars) != null or bun.strings.indexAnyComptime(display_version, banned_path_chars) != null;
const has_banned_char = strings.indexAnyComptime(update_request.name, banned_path_chars) != null or strings.indexAnyComptime(display_version, banned_path_chars) != null;

break :brk try if (has_banned_char)
// This branch gets hit usually when a URL is requested as the package
Expand Down Expand Up @@ -503,8 +511,10 @@ pub const BunxCommand = struct {
const passthrough = opts.passthrough_list.items;

var do_cache_bust = update_request.version.tag == .dist_tag;
const look_for_existing_bin = update_request.version.literal.isEmpty() or update_request.version.tag != .dist_tag;

if (update_request.version.literal.isEmpty() or update_request.version.tag != .dist_tag) try_run_existing: {
debug("try run existing? {}", .{look_for_existing_bin});
if (look_for_existing_bin) try_run_existing: {
var destination_: ?[:0]const u8 = null;

// Only use the system-installed version if there is no version specified
Expand Down Expand Up @@ -563,11 +573,17 @@ pub const BunxCommand = struct {
};

if (is_stale) {
debug("found stale binary: {s}", .{out});
do_cache_bust = true;
break :try_run_existing;
if (opts.no_install) {
Output.warn("Using a stale installation of <b>{s}<r> because --no-install was passed. Run `bunx` without --no-install to use a fresh binary.", .{update_request.name});
} else {
break :try_run_existing;
}
}
}

debug("running existing binary: {s}", .{destination});
try Run.runBinary(
ctx,
try this_transpiler.fs.dirname_store.append(@TypeOf(out), out),
Expand Down Expand Up @@ -626,6 +642,24 @@ pub const BunxCommand = struct {
}
}
}
// If we've reached this point, it means we couldn't find an existing binary to run.
// Next step is to install, then run it.

// NOTE: npx prints errors like this:
//
// npm error npx canceled due to missing packages and no YES option: ["[email protected]"]
// npm error A complete log of this run can be found in: [folder]/debug.log
//
// Which is not very helpful.

if (opts.no_install) {
Output.errGeneric(
"Could not find an existing '{s}' binary to run. Stopping because --no-install was passed.",
.{initial_bin_name},
);
Global.exit(1);
}

const bunx_install_dir = try std.fs.cwd().makeOpenPath(bunx_cache_dir, .{});

create_package_json: {
Expand Down
80 changes: 77 additions & 3 deletions test/cli/install/bunx.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { spawn } from "bun";
import { beforeAll, beforeEach, expect, it, setDefaultTimeout } from "bun:test";
import { describe, beforeAll, beforeEach, expect, it, setDefaultTimeout } from "bun:test";
import { rm, writeFile } from "fs/promises";
import { bunEnv, bunExe, isWindows, tmpdirSync, readdirSorted } from "harness";
import { readdirSync } from "node:fs";
Expand Down Expand Up @@ -83,7 +83,7 @@ it("should choose the tagged versions instead of the PATH versions when a tag is
cwd: x_dir,
stdout: "pipe",
stdin: "ignore",
stderr: "inherit",
stderr: "ignore",
env: {
...env,
// BUN_DEBUG_QUIET_LOGS: undefined,
Expand Down Expand Up @@ -280,7 +280,7 @@ it("should work for github repository with committish", async () => {

// cached
const cached = spawn({
cmd: [bunExe(), "x", "github:piuccio/cowsay#HEAD", "hello bun!"],
cmd: [bunExe(), "x", "--no-install", "github:piuccio/cowsay#HEAD", "hello bun!"],
cwd: x_dir,
stdout: "pipe",
stdin: "inherit",
Expand Down Expand Up @@ -396,3 +396,77 @@ it('should set "npm_config_user_agent" to bun', async () => {
expect(out.trim()).toContain(`bun/${Bun.version}`);
expect(exited).toBe(0);
});

/**
* IMPORTANT
* Please only use packages with small unpacked sizes for tests. It helps keep them fast.
*/
describe("bunx --no-install", () => {
const run = (...args: string[]): Promise<[stderr: string, stdout: string, exitCode: number]> => {
const subprocess = spawn({
cmd: [bunExe(), "x", ...args],
cwd: x_dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});

return Promise.all([
new Response(subprocess.stderr).text(),
new Response(subprocess.stdout).text(),
subprocess.exited,
] as const);
};

it("if the package is not installed, it should fail and print an error message", async () => {
const [err, out, exited] = await run("--no-install", "http-server", "--version");

expect(err.trim()).toContain(
"Could not find an existing 'http-server' binary to run.",
);
expect(out).toHaveLength(0);
expect(exited).toBe(1);
});

/*
yes, multiple package tests are neccessary.
1. there's specialized logic for `bunx tsc` and `bunx typescript`
2. http-server checks for non-alphanumeric edge cases. Plus it's small
3. eslint is alphanumeric and extremely common
*/
it.each(["typescript", "http-server", "eslint"])("`bunx --no-install %s` should find cached packages", async pkg => {
// not cached
{
const [err, out, code] = await run(pkg, "--version");
expect(err).not.toContain("error:");
expect(out).not.toBeEmpty();
expect(code).toBe(0);
}

// cached
{
const [err, out, code] = await run("--no-install", pkg, "--version");
expect(err).not.toContain("error:");
expect(out).not.toBeEmpty();
expect(code).toBe(0);
}
});

it("when an exact version match is found, should find cached packages", async () => {
// not cached
{
const [err, out, code] = await run("[email protected]", "--version");
expect(err).not.toContain("error:");
expect(out).not.toBeEmpty();
expect(code).toBe(0);
}

// cached
{
const [err, out, code] = await run("--no-install", "[email protected]", "--version");
expect(err).not.toContain("error:");
expect(out).not.toBeEmpty();
expect(code).toBe(0);
}
});
});

0 comments on commit 258aee7

Please sign in to comment.