mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 10:58:56 +00:00
1059 lines
43 KiB
Zig
1059 lines
43 KiB
Zig
const ExternalStringList = @import("./install.zig").ExternalStringList;
|
|
const Semver = bun.Semver;
|
|
const ExternalString = Semver.ExternalString;
|
|
const String = Semver.String;
|
|
const Output = bun.Output;
|
|
const Global = bun.Global;
|
|
const std = @import("std");
|
|
const strings = bun.strings;
|
|
const Environment = @import("../env.zig");
|
|
const C = @import("../c.zig");
|
|
const Fs = @import("../fs.zig");
|
|
const stringZ = bun.stringZ;
|
|
const Resolution = @import("./resolution.zig").Resolution;
|
|
const bun = @import("root").bun;
|
|
const path = bun.path;
|
|
const string = bun.string;
|
|
const Install = @import("./install.zig");
|
|
const PackageInstall = Install.PackageInstall;
|
|
const Dependency = @import("./dependency.zig");
|
|
const OOM = bun.OOM;
|
|
const JSON = bun.JSON;
|
|
const Lockfile = Install.Lockfile;
|
|
|
|
/// Normalized `bin` field in [package.json](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#bin)
|
|
/// Can be a:
|
|
/// - file path (relative to the package root)
|
|
/// - directory (relative to the package root)
|
|
/// - map where keys are names of the binaries and values are file paths to the binaries
|
|
pub const Bin = extern struct {
|
|
tag: Tag = Tag.none,
|
|
_padding_tag: [3]u8 = .{0} ** 3,
|
|
|
|
// Largest member must be zero initialized
|
|
value: Value = Value{ .map = ExternalStringList{} },
|
|
|
|
pub fn count(this: *const Bin, buf: []const u8, extern_strings: []const ExternalString, comptime StringBuilder: type, builder: StringBuilder) u32 {
|
|
switch (this.tag) {
|
|
.file => builder.count(this.value.file.slice(buf)),
|
|
.named_file => {
|
|
builder.count(this.value.named_file[0].slice(buf));
|
|
builder.count(this.value.named_file[1].slice(buf));
|
|
},
|
|
.dir => builder.count(this.value.dir.slice(buf)),
|
|
.map => {
|
|
const list = this.value.map.get(extern_strings);
|
|
for (list) |*extern_string| {
|
|
builder.count(extern_string.slice(buf));
|
|
}
|
|
return @as(u32, @truncate(list.len));
|
|
},
|
|
else => {},
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
pub fn eql(
|
|
l: *const Bin,
|
|
r: *const Bin,
|
|
l_buf: string,
|
|
l_extern_strings: []const ExternalString,
|
|
r_buf: string,
|
|
r_extern_strings: []const ExternalString,
|
|
) bool {
|
|
if (l.tag != r.tag) return false;
|
|
|
|
return switch (l.tag) {
|
|
.none => true,
|
|
.file => l.value.file.eql(r.value.file, l_buf, r_buf),
|
|
.dir => l.value.dir.eql(r.value.dir, l_buf, r_buf),
|
|
.named_file => l.value.named_file[0].eql(r.value.named_file[0], l_buf, r_buf) and
|
|
l.value.named_file[1].eql(r.value.named_file[1], l_buf, r_buf),
|
|
.map => {
|
|
const l_list = l.value.map.get(l_extern_strings);
|
|
const r_list = r.value.map.get(r_extern_strings);
|
|
if (l_list.len != r_list.len) return false;
|
|
|
|
// assuming these maps are small without duplicate keys
|
|
var i: usize = 0;
|
|
outer: while (i < l_list.len) : (i += 2) {
|
|
var j: usize = 0;
|
|
while (j < r_list.len) : (j += 2) {
|
|
if (l_list[i].hash == r_list[j].hash) {
|
|
if (l_list[i + 1].hash != r_list[j + 1].hash) {
|
|
return false;
|
|
}
|
|
|
|
continue :outer;
|
|
}
|
|
}
|
|
|
|
// not found
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
};
|
|
}
|
|
|
|
pub fn clone(this: *const Bin, buf: []const u8, prev_external_strings: []const ExternalString, all_extern_strings: []ExternalString, extern_strings_slice: []ExternalString, comptime StringBuilder: type, builder: StringBuilder) Bin {
|
|
switch (this.tag) {
|
|
.none => {
|
|
return Bin{
|
|
.tag = .none,
|
|
.value = Value.init(.{ .none = {} }),
|
|
};
|
|
},
|
|
.file => {
|
|
return Bin{
|
|
.tag = .file,
|
|
.value = Value.init(.{ .file = builder.append(String, this.value.file.slice(buf)) }),
|
|
};
|
|
},
|
|
.named_file => {
|
|
return Bin{
|
|
.tag = .named_file,
|
|
.value = Value.init(
|
|
.{
|
|
.named_file = [2]String{
|
|
builder.append(String, this.value.named_file[0].slice(buf)),
|
|
builder.append(String, this.value.named_file[1].slice(buf)),
|
|
},
|
|
},
|
|
),
|
|
};
|
|
},
|
|
.dir => {
|
|
return Bin{
|
|
.tag = .dir,
|
|
.value = Value.init(.{ .dir = builder.append(String, this.value.dir.slice(buf)) }),
|
|
};
|
|
},
|
|
.map => {
|
|
for (this.value.map.get(prev_external_strings), 0..) |extern_string, i| {
|
|
extern_strings_slice[i] = builder.append(ExternalString, extern_string.slice(buf));
|
|
}
|
|
|
|
return Bin{
|
|
.tag = .map,
|
|
.value = Value.init(.{ .map = ExternalStringList.init(all_extern_strings, extern_strings_slice) }),
|
|
};
|
|
},
|
|
}
|
|
|
|
unreachable;
|
|
}
|
|
|
|
pub fn cloneAppend(this: *const Bin, this_buf: string, this_extern_strings: []const ExternalString, lockfile: *Lockfile) OOM!Bin {
|
|
var string_buf = lockfile.stringBuf();
|
|
defer string_buf.apply(lockfile);
|
|
|
|
const cloned: Bin = .{
|
|
.tag = this.tag,
|
|
|
|
.value = switch (this.tag) {
|
|
.none => Value.init(.{ .none = {} }),
|
|
.file => Value.init(.{
|
|
.file = try string_buf.append(this.value.file.slice(this_buf)),
|
|
}),
|
|
.named_file => Value.init(.{ .named_file = .{
|
|
try string_buf.append(this.value.named_file[0].slice(this_buf)),
|
|
try string_buf.append(this.value.named_file[1].slice(this_buf)),
|
|
} }),
|
|
.dir => Value.init(.{
|
|
.dir = try string_buf.append(this.value.dir.slice(this_buf)),
|
|
}),
|
|
.map => map: {
|
|
const off = lockfile.buffers.extern_strings.items.len;
|
|
for (this.value.map.get(this_extern_strings)) |extern_string| {
|
|
try lockfile.buffers.extern_strings.append(
|
|
lockfile.allocator,
|
|
try string_buf.appendExternal(extern_string.slice(this_buf)),
|
|
);
|
|
}
|
|
const new = lockfile.buffers.extern_strings.items[off..];
|
|
break :map Value.init(.{
|
|
.map = ExternalStringList.init(lockfile.buffers.extern_strings.items, new),
|
|
});
|
|
},
|
|
},
|
|
};
|
|
|
|
return cloned;
|
|
}
|
|
|
|
/// Used for packages read from text lockfile.
|
|
pub fn parseAppend(
|
|
allocator: std.mem.Allocator,
|
|
bin_expr: JSON.Expr,
|
|
buf: *String.Buf,
|
|
extern_strings: *std.ArrayListUnmanaged(ExternalString),
|
|
) OOM!Bin {
|
|
switch (bin_expr.data) {
|
|
.e_object => |obj| {
|
|
switch (obj.properties.len) {
|
|
0 => {},
|
|
1 => {
|
|
const bin_name = obj.properties.ptr[0].key.?.asString(allocator) orelse return .{};
|
|
const value = obj.properties.ptr[0].value.?.asString(allocator) orelse return .{};
|
|
|
|
return .{
|
|
.tag = .named_file,
|
|
.value = .{
|
|
.named_file = .{
|
|
try buf.append(bin_name),
|
|
try buf.append(value),
|
|
},
|
|
},
|
|
};
|
|
},
|
|
else => {
|
|
const current_len = extern_strings.items.len;
|
|
const num_props: usize = obj.properties.len * 2;
|
|
try extern_strings.ensureTotalCapacityPrecise(
|
|
allocator,
|
|
current_len + num_props,
|
|
);
|
|
var new = extern_strings.items.ptr[current_len .. current_len + num_props];
|
|
extern_strings.items.len += num_props;
|
|
|
|
var i: usize = 0;
|
|
for (obj.properties.slice()) |bin_prop| {
|
|
const key = bin_prop.key.?;
|
|
const value = bin_prop.value.?;
|
|
const key_str = key.asString(allocator) orelse return .{};
|
|
const value_str = value.asString(allocator) orelse return .{};
|
|
new[i] = try buf.appendExternal(key_str);
|
|
i += 1;
|
|
new[i] = try buf.appendExternal(value_str);
|
|
i += 1;
|
|
}
|
|
if (comptime Environment.allow_assert) {
|
|
bun.assert(i == new.len);
|
|
}
|
|
return .{
|
|
.tag = .map,
|
|
.value = .{
|
|
.map = ExternalStringList.init(extern_strings.items, new),
|
|
},
|
|
};
|
|
},
|
|
}
|
|
},
|
|
.e_string => |str| {
|
|
if (str.data.len > 0) {
|
|
return .{
|
|
.tag = .file,
|
|
.value = .{
|
|
.file = try buf.append(str.data),
|
|
},
|
|
};
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
return .{};
|
|
}
|
|
|
|
pub fn parseAppendFromDirectories(allocator: std.mem.Allocator, bin_expr: JSON.Expr, buf: *String.Buf) OOM!Bin {
|
|
if (bin_expr.asString(allocator)) |bin_str| {
|
|
return .{
|
|
.tag = .dir,
|
|
.value = .{
|
|
.dir = try buf.append(bin_str),
|
|
},
|
|
};
|
|
}
|
|
return .{};
|
|
}
|
|
|
|
pub fn toJson(
|
|
this: *const Bin,
|
|
comptime style: enum { single_line, multi_line },
|
|
indent: if (style == .multi_line) *u32 else void,
|
|
buf: string,
|
|
extern_strings: []const ExternalString,
|
|
writer: anytype,
|
|
writeIndent: *const fn (anytype, *u32) @TypeOf(writer).Error!void,
|
|
) @TypeOf(writer).Error!void {
|
|
bun.debugAssert(this.tag != .none);
|
|
if (comptime style == .single_line) {
|
|
switch (this.tag) {
|
|
.none => {},
|
|
.file => {
|
|
try writer.print("{}", .{this.value.file.fmtJson(buf, .{})});
|
|
},
|
|
.named_file => {
|
|
try writer.writeByte('{');
|
|
try writer.print(" {}: {} ", .{
|
|
this.value.named_file[0].fmtJson(buf, .{}),
|
|
this.value.named_file[1].fmtJson(buf, .{}),
|
|
});
|
|
try writer.writeByte('}');
|
|
},
|
|
.dir => {
|
|
try writer.print("{}", .{this.value.dir.fmtJson(buf, .{})});
|
|
},
|
|
.map => {
|
|
try writer.writeByte('{');
|
|
const list = this.value.map.get(extern_strings);
|
|
var first = true;
|
|
var i: usize = 0;
|
|
while (i < list.len) : (i += 2) {
|
|
if (!first) {
|
|
try writer.writeByte(',');
|
|
}
|
|
first = false;
|
|
try writer.print(" {}: {}", .{
|
|
list[i].value.fmtJson(buf, .{}),
|
|
list[i + 1].value.fmtJson(buf, .{}),
|
|
});
|
|
}
|
|
try writer.writeAll(" }");
|
|
},
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
switch (this.tag) {
|
|
.none => {},
|
|
.file => {
|
|
try writer.print("{}", .{this.value.file.fmtJson(buf, .{})});
|
|
},
|
|
.named_file => {
|
|
try writer.writeAll("{\n");
|
|
indent.* += 1;
|
|
try writeIndent(writer, indent);
|
|
try writer.print("{}: {},\n", .{
|
|
this.value.named_file[0].fmtJson(buf, .{}),
|
|
this.value.named_file[1].fmtJson(buf, .{}),
|
|
});
|
|
indent.* -= 1;
|
|
try writeIndent(writer, indent);
|
|
try writer.writeByte('}');
|
|
},
|
|
.dir => {
|
|
try writer.print("{}", .{this.value.dir.fmtJson(buf, .{})});
|
|
},
|
|
.map => {
|
|
try writer.writeByte('{');
|
|
indent.* += 1;
|
|
|
|
const list = this.value.map.get(extern_strings);
|
|
var any = false;
|
|
var i: usize = 0;
|
|
while (i < list.len) : (i += 2) {
|
|
if (!any) {
|
|
any = true;
|
|
try writer.writeByte('\n');
|
|
}
|
|
try writeIndent(writer, indent);
|
|
try writer.print("{}: {},\n", .{
|
|
list[i].value.fmtJson(buf, .{}),
|
|
list[i + 1].value.fmtJson(buf, .{}),
|
|
});
|
|
}
|
|
if (!any) {
|
|
try writer.writeByte('}');
|
|
indent.* -= 1;
|
|
return;
|
|
}
|
|
|
|
indent.* -= 1;
|
|
try writeIndent(writer, indent);
|
|
try writer.writeByte('}');
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn init() Bin {
|
|
return bun.serializable(Bin{ .tag = .none, .value = Value.init(.{ .none = {} }) });
|
|
}
|
|
|
|
pub const Value = extern union {
|
|
/// no "bin", or empty "bin"
|
|
none: void,
|
|
|
|
/// "bin" is a string
|
|
/// ```
|
|
/// "bin": "./bin/foo",
|
|
/// ```
|
|
file: String,
|
|
|
|
// Single-entry map
|
|
///```
|
|
/// "bin": {
|
|
/// "babel": "./cli.js",
|
|
/// }
|
|
///```
|
|
named_file: [2]String,
|
|
|
|
/// "bin" is a directory
|
|
///```
|
|
/// "dirs": {
|
|
/// "bin": "./bin",
|
|
/// }
|
|
///```
|
|
dir: String,
|
|
// "bin" is a map
|
|
///```
|
|
/// "bin": {
|
|
/// "babel": "./cli.js",
|
|
/// "babel-cli": "./cli.js",
|
|
/// }
|
|
///```
|
|
map: ExternalStringList,
|
|
|
|
/// To avoid undefined memory between union values, we must zero initialize the union first.
|
|
pub fn init(field: anytype) Value {
|
|
return bun.serializableInto(Value, field);
|
|
}
|
|
};
|
|
|
|
pub const Tag = enum(u8) {
|
|
/// no bin field
|
|
none = 0,
|
|
|
|
/// "bin" is a string
|
|
/// ```
|
|
/// "bin": "./bin/foo",
|
|
/// ```
|
|
file = 1,
|
|
|
|
// Single-entry map
|
|
///```
|
|
/// "bin": {
|
|
/// "babel": "./cli.js",
|
|
/// }
|
|
///```
|
|
named_file = 2,
|
|
|
|
/// "bin" is a directory
|
|
///```
|
|
/// "dirs": {
|
|
/// "bin": "./bin",
|
|
/// }
|
|
///```
|
|
dir = 3,
|
|
|
|
// "bin" is a map of more than one
|
|
///```
|
|
/// "bin": {
|
|
/// "babel": "./cli.js",
|
|
/// "babel-cli": "./cli.js",
|
|
/// "webpack-dev-server": "./cli.js",
|
|
/// }
|
|
///```
|
|
map = 4,
|
|
};
|
|
|
|
pub const NamesIterator = struct {
|
|
bin: Bin,
|
|
i: usize = 0,
|
|
done: bool = false,
|
|
dir_iterator: ?std.fs.Dir.Iterator = null,
|
|
package_name: String,
|
|
destination_node_modules: std.fs.Dir = bun.invalid_fd.asDir(),
|
|
buf: bun.PathBuffer = undefined,
|
|
string_buffer: []const u8,
|
|
extern_string_buf: []const ExternalString,
|
|
|
|
fn nextInDir(this: *NamesIterator) !?[]const u8 {
|
|
if (this.done) return null;
|
|
if (this.dir_iterator == null) {
|
|
var target = this.bin.value.dir.slice(this.string_buffer);
|
|
if (strings.hasPrefixComptime(target, "./") or strings.hasPrefixComptime(target, ".\\")) {
|
|
target = target[2..];
|
|
}
|
|
var parts = [_][]const u8{ this.package_name.slice(this.string_buffer), target };
|
|
|
|
const dir = this.destination_node_modules;
|
|
|
|
const joined = path.joinStringBuf(&this.buf, &parts, .auto);
|
|
this.buf[joined.len] = 0;
|
|
const joined_: [:0]u8 = this.buf[0..joined.len :0];
|
|
var child_dir = try bun.openDir(dir, joined_);
|
|
this.dir_iterator = child_dir.iterate();
|
|
}
|
|
|
|
var iter = &this.dir_iterator.?;
|
|
if (iter.next() catch null) |entry| {
|
|
this.i += 1;
|
|
return entry.name;
|
|
} else {
|
|
this.done = true;
|
|
this.dir_iterator.?.dir.close();
|
|
this.dir_iterator = null;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// next filename, e.g. "babel" instead of "cli.js"
|
|
pub fn next(this: *NamesIterator) !?[]const u8 {
|
|
switch (this.bin.tag) {
|
|
.file => {
|
|
if (this.i > 0) return null;
|
|
this.i += 1;
|
|
this.done = true;
|
|
const base = std.fs.path.basename(this.package_name.slice(this.string_buffer));
|
|
if (strings.hasPrefixComptime(base, "./") or strings.hasPrefixComptime(base, ".\\"))
|
|
return strings.copy(&this.buf, base[2..]);
|
|
|
|
return strings.copy(&this.buf, base);
|
|
},
|
|
.named_file => {
|
|
if (this.i > 0) return null;
|
|
this.i += 1;
|
|
this.done = true;
|
|
const base = std.fs.path.basename(this.bin.value.named_file[0].slice(this.string_buffer));
|
|
if (strings.hasPrefixComptime(base, "./") or strings.hasPrefixComptime(base, ".\\"))
|
|
return strings.copy(&this.buf, base[2..]);
|
|
return strings.copy(&this.buf, base);
|
|
},
|
|
|
|
.dir => return try this.nextInDir(),
|
|
.map => {
|
|
if (this.i >= this.bin.value.map.len) return null;
|
|
const index = this.i;
|
|
this.i += 2;
|
|
this.done = this.i >= this.bin.value.map.len;
|
|
const current_string = this.bin.value.map.get(
|
|
this.extern_string_buf,
|
|
)[index];
|
|
|
|
const base = std.fs.path.basename(
|
|
current_string.slice(
|
|
this.string_buffer,
|
|
),
|
|
);
|
|
if (strings.hasPrefixComptime(base, "./") or strings.hasPrefixComptime(base, ".\\"))
|
|
return strings.copy(&this.buf, base[2..]);
|
|
return strings.copy(&this.buf, base);
|
|
},
|
|
else => return null,
|
|
}
|
|
}
|
|
};
|
|
|
|
pub const PriorityQueueContext = struct {
|
|
dependencies: *const std.ArrayListUnmanaged(Dependency),
|
|
string_buf: *const std.ArrayListUnmanaged(u8),
|
|
|
|
pub fn lessThan(this: PriorityQueueContext, a: Install.DependencyID, b: Install.DependencyID) std.math.Order {
|
|
const deps = this.dependencies.items;
|
|
const buf = this.string_buf.items;
|
|
const a_name = deps[a].name.slice(buf);
|
|
const b_name = deps[b].name.slice(buf);
|
|
return strings.order(a_name, b_name);
|
|
}
|
|
};
|
|
|
|
pub const PriorityQueue = std.PriorityQueue(Install.DependencyID, PriorityQueueContext, PriorityQueueContext.lessThan);
|
|
|
|
// https://github.com/npm/npm-normalize-package-bin/blob/574e6d7cd21b2f3dee28a216ec2053c2551f7af9/lib/index.js#L38
|
|
pub fn normalizedBinName(name: []const u8) []const u8 {
|
|
if (std.mem.lastIndexOfAny(u8, name, "/\\:")) |i| {
|
|
return name[i + 1 ..];
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
pub const Linker = struct {
|
|
bin: Bin,
|
|
|
|
// Hash map of seen destination paths for this `node_modules/.bin` folder. PackageInstaller will reset it before
|
|
// linking each tree.
|
|
seen: ?*bun.StringHashMap(void),
|
|
|
|
node_modules: bun.FileDescriptor,
|
|
node_modules_path: []const u8,
|
|
|
|
/// Used for generating relative paths
|
|
package_name: strings.StringOrTinyString,
|
|
|
|
global_bin_path: stringZ = "",
|
|
|
|
string_buf: []const u8,
|
|
extern_string_buf: []const ExternalString,
|
|
|
|
abs_target_buf: []u8,
|
|
abs_dest_buf: []u8,
|
|
rel_buf: []u8,
|
|
|
|
err: ?anyerror = null,
|
|
|
|
pub var umask: bun.C.Mode = 0;
|
|
|
|
var has_set_umask = false;
|
|
|
|
pub fn ensureUmask() void {
|
|
if (!has_set_umask) {
|
|
has_set_umask = true;
|
|
umask = bun.C.umask(0);
|
|
}
|
|
}
|
|
|
|
fn unlinkBinOrShim(abs_dest: [:0]const u8) void {
|
|
if (comptime !Environment.isWindows) {
|
|
_ = bun.sys.unlink(abs_dest);
|
|
return;
|
|
}
|
|
|
|
var dest_buf: bun.WPathBuffer = undefined;
|
|
const abs_dest_w = strings.convertUTF8toUTF16InBuffer(&dest_buf, abs_dest);
|
|
@memcpy(dest_buf[abs_dest_w.len..][0..".bunx\x00".len], comptime strings.literal(u16, ".bunx\x00"));
|
|
const abs_bunx_file: [:0]const u16 = dest_buf[0 .. abs_dest_w.len + ".bunx".len :0];
|
|
_ = bun.sys.unlinkW(abs_bunx_file);
|
|
@memcpy(dest_buf[abs_dest_w.len..][0..".exe\x00".len], comptime strings.literal(u16, ".exe\x00"));
|
|
const abs_exe_file: [:0]const u16 = dest_buf[0 .. abs_dest_w.len + ".exe".len :0];
|
|
_ = bun.sys.unlinkW(abs_exe_file);
|
|
}
|
|
|
|
fn linkBinOrCreateShim(this: *Linker, abs_target: [:0]const u8, abs_dest: [:0]const u8, global: bool) void {
|
|
bun.assertWithLocation(std.fs.path.isAbsolute(abs_target), @src());
|
|
bun.assertWithLocation(std.fs.path.isAbsolute(abs_dest), @src());
|
|
bun.assertWithLocation(abs_target[abs_target.len - 1] != std.fs.path.sep, @src());
|
|
bun.assertWithLocation(abs_dest[abs_dest.len - 1] != std.fs.path.sep, @src());
|
|
|
|
if (this.seen) |seen| {
|
|
// Skip seen destinations for this tree
|
|
// https://github.com/npm/cli/blob/22731831e22011e32fa0ca12178e242c2ee2b33d/node_modules/bin-links/lib/link-gently.js#L30
|
|
const entry = seen.getOrPut(abs_dest) catch bun.outOfMemory();
|
|
if (entry.found_existing) {
|
|
return;
|
|
}
|
|
entry.key_ptr.* = seen.allocator.dupe(u8, abs_dest) catch bun.outOfMemory();
|
|
}
|
|
|
|
// Skip if the target does not exist. This is important because placing a dangling
|
|
// shim in path might break a postinstall
|
|
if (!bun.sys.exists(abs_target)) {
|
|
return;
|
|
}
|
|
|
|
bun.Analytics.Features.binlinks += 1;
|
|
|
|
if (comptime !Environment.isWindows)
|
|
this.createSymlink(abs_target, abs_dest, global)
|
|
else {
|
|
const target = bun.sys.openat(bun.invalid_fd, abs_target, bun.O.RDONLY, 0).unwrap() catch |err| {
|
|
if (err != error.EISDIR) {
|
|
// ignore directories, creating a shim for one won't do anything
|
|
this.err = err;
|
|
}
|
|
return;
|
|
};
|
|
defer _ = bun.sys.close(target);
|
|
this.createWindowsShim(target, abs_target, abs_dest, global);
|
|
}
|
|
|
|
if (this.err != null) {
|
|
// cleanup on error just in case
|
|
unlinkBinOrShim(abs_dest);
|
|
return;
|
|
}
|
|
|
|
if (comptime !Environment.isWindows) {
|
|
// any error here is ignored
|
|
const bin = bun.sys.File.openat(bun.invalid_fd, abs_target, bun.O.RDWR, 0o664).unwrap() catch return;
|
|
defer bin.close();
|
|
|
|
var shebang_buf: [1024]u8 = undefined;
|
|
const read = bin.read(&shebang_buf).unwrap() catch return;
|
|
const chunk = shebang_buf[0..read];
|
|
// 123 4 5
|
|
// #!a\r\n
|
|
if (chunk.len < 5 or chunk[0] != '#' or chunk[1] != '!') return;
|
|
|
|
if (strings.indexOfChar(chunk, '\n')) |newline| {
|
|
if (newline > 0 and chunk[newline - 1] == '\r') {
|
|
const pos = newline - 1;
|
|
bin.handle.asFile().seekTo(pos) catch return;
|
|
bin.writeAll("\n").unwrap() catch return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn createWindowsShim(this: *Linker, target: bun.FileDescriptor, abs_target: [:0]const u8, abs_dest: [:0]const u8, global: bool) void {
|
|
const WinBinLinkingShim = @import("./windows-shim/BinLinkingShim.zig");
|
|
|
|
var shim_buf: [65536]u8 = undefined;
|
|
var read_in_buf: [WinBinLinkingShim.Shebang.max_shebang_input_length]u8 = undefined;
|
|
var dest_buf: bun.WPathBuffer = undefined;
|
|
var target_buf: bun.WPathBuffer = undefined;
|
|
|
|
const abs_dest_w = strings.convertUTF8toUTF16InBuffer(&dest_buf, abs_dest);
|
|
@memcpy(dest_buf[abs_dest_w.len..][0..".bunx\x00".len], comptime strings.literal(u16, ".bunx\x00"));
|
|
|
|
const abs_bunx_file: [:0]const u16 = dest_buf[0 .. abs_dest_w.len + ".bunx".len :0];
|
|
|
|
const bunx_file = bun.sys.File.openatOSPath(bun.invalid_fd, abs_bunx_file, bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC, 0o664).unwrap() catch |err| bunx_file: {
|
|
if (err != error.ENOENT or global) {
|
|
this.err = err;
|
|
return;
|
|
}
|
|
|
|
bun.makePath(this.node_modules.asDir(), ".bin") catch {};
|
|
break :bunx_file bun.sys.File.openatOSPath(bun.invalid_fd, abs_bunx_file, bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC, 0o664).unwrap() catch |real_err| {
|
|
this.err = real_err;
|
|
return;
|
|
};
|
|
};
|
|
defer bunx_file.close();
|
|
|
|
const rel_target = path.relativeBufZ(this.rel_buf, path.dirname(abs_dest, .auto), abs_target);
|
|
bun.assertWithLocation(strings.hasPrefixComptime(rel_target, "..\\"), @src());
|
|
|
|
const rel_target_w = strings.toWPathNormalized(&target_buf, rel_target["..\\".len..]);
|
|
|
|
const shebang = shebang: {
|
|
const first_content_chunk = contents: {
|
|
const reader = target.asFile().reader();
|
|
const read = reader.read(&read_in_buf) catch break :contents null;
|
|
if (read == 0) break :contents null;
|
|
break :contents read_in_buf[0..read];
|
|
};
|
|
|
|
if (first_content_chunk) |chunk| {
|
|
break :shebang WinBinLinkingShim.Shebang.parse(chunk, rel_target_w) catch {
|
|
this.err = error.InvalidBinCount;
|
|
return;
|
|
};
|
|
} else {
|
|
break :shebang WinBinLinkingShim.Shebang.parseFromBinPath(rel_target_w);
|
|
}
|
|
};
|
|
|
|
const shim = WinBinLinkingShim{
|
|
.bin_path = rel_target_w,
|
|
.shebang = shebang,
|
|
};
|
|
|
|
const len = shim.encodedLength();
|
|
if (len > shim_buf.len) {
|
|
this.err = error.InvalidBinContent;
|
|
return;
|
|
}
|
|
|
|
const metadata = shim_buf[0..len];
|
|
shim.encodeInto(metadata) catch {
|
|
this.err = error.InvalidBinContent;
|
|
return;
|
|
};
|
|
|
|
bunx_file.writer().writeAll(metadata) catch |err| {
|
|
this.err = err;
|
|
return;
|
|
};
|
|
|
|
@memcpy(dest_buf[abs_dest_w.len..][0..".exe\x00".len], comptime strings.literal(u16, ".exe\x00"));
|
|
const abs_exe_file: [:0]const u16 = dest_buf[0 .. abs_dest_w.len + ".exe".len :0];
|
|
|
|
bun.sys.File.writeFile(bun.invalid_fd, abs_exe_file, WinBinLinkingShim.embedded_executable_data).unwrap() catch |err| {
|
|
if (err == error.EBUSY) {
|
|
// exe is most likely running. bunx file has already been updated, ignore error
|
|
return;
|
|
}
|
|
|
|
this.err = err;
|
|
return;
|
|
};
|
|
}
|
|
|
|
fn createSymlink(this: *Linker, abs_target: [:0]const u8, abs_dest: [:0]const u8, global: bool) void {
|
|
defer {
|
|
if (this.err == null) {
|
|
_ = bun.sys.chmod(abs_target, umask | 0o777);
|
|
}
|
|
}
|
|
|
|
const abs_dest_dir = path.dirname(abs_dest, .auto);
|
|
const rel_target = path.relativeBufZ(this.rel_buf, abs_dest_dir, abs_target);
|
|
|
|
bun.assertWithLocation(strings.hasPrefixComptime(rel_target, ".."), @src());
|
|
|
|
switch (bun.sys.symlink(rel_target, abs_dest)) {
|
|
.err => |err| {
|
|
if (err.getErrno() != .EXIST and err.getErrno() != .NOENT) {
|
|
this.err = err.toZigErr();
|
|
return;
|
|
}
|
|
|
|
// ENOENT means `.bin` hasn't been created yet. Should only happen if this isn't global
|
|
if (err.getErrno() == .NOENT) {
|
|
if (global) {
|
|
this.err = err.toZigErr();
|
|
return;
|
|
}
|
|
|
|
bun.makePath(this.node_modules.asDir(), ".bin") catch {};
|
|
switch (bun.sys.symlink(rel_target, abs_dest)) {
|
|
.err => |real_error| {
|
|
// It was just created, no need to delete destination and symlink again
|
|
this.err = real_error.toZigErr();
|
|
return;
|
|
},
|
|
.result => return,
|
|
}
|
|
bun.sys.symlink(rel_target, abs_dest).unwrap() catch |real_err| {
|
|
this.err = real_err;
|
|
};
|
|
return;
|
|
}
|
|
|
|
// beyond this error can only be `.EXIST`
|
|
bun.assertWithLocation(err.getErrno() == .EXIST, @src());
|
|
},
|
|
.result => return,
|
|
}
|
|
|
|
// delete and try again
|
|
std.fs.deleteTreeAbsolute(abs_dest) catch {};
|
|
bun.sys.symlink(rel_target, abs_dest).unwrap() catch |err| {
|
|
this.err = err;
|
|
};
|
|
}
|
|
|
|
/// uses `this.abs_target_buf`
|
|
pub fn buildTargetPackageDir(this: *const Linker) []const u8 {
|
|
const dest_dir_without_trailing_slash = strings.withoutTrailingSlash(this.node_modules_path);
|
|
|
|
var remain = this.abs_target_buf;
|
|
|
|
@memcpy(remain[0..dest_dir_without_trailing_slash.len], dest_dir_without_trailing_slash);
|
|
remain = remain[dest_dir_without_trailing_slash.len..];
|
|
remain[0] = std.fs.path.sep;
|
|
remain = remain[1..];
|
|
|
|
const package_name = this.package_name.slice();
|
|
@memcpy(remain[0..package_name.len], package_name);
|
|
remain = remain[package_name.len..];
|
|
remain[0] = std.fs.path.sep;
|
|
remain = remain[1..];
|
|
|
|
return this.abs_target_buf[0 .. @intFromPtr(remain.ptr) - @intFromPtr(this.abs_target_buf.ptr)];
|
|
}
|
|
|
|
pub fn buildDestinationDir(this: *const Linker, global: bool) []u8 {
|
|
const dest_dir_without_trailing_slash = strings.withoutTrailingSlash(this.node_modules_path);
|
|
|
|
var remain = this.abs_dest_buf;
|
|
if (global) {
|
|
const global_bin_path_without_trailing_slash = strings.withoutTrailingSlash(this.global_bin_path);
|
|
@memcpy(remain[0..global_bin_path_without_trailing_slash.len], global_bin_path_without_trailing_slash);
|
|
remain = remain[global_bin_path_without_trailing_slash.len..];
|
|
remain[0] = std.fs.path.sep;
|
|
remain = remain[1..];
|
|
} else {
|
|
@memcpy(remain[0..dest_dir_without_trailing_slash.len], dest_dir_without_trailing_slash);
|
|
remain = remain[dest_dir_without_trailing_slash.len..];
|
|
@memcpy(remain[0.."/.bin/".len], std.fs.path.sep_str ++ ".bin" ++ std.fs.path.sep_str);
|
|
remain = remain["/.bin/".len..];
|
|
}
|
|
|
|
return remain;
|
|
}
|
|
|
|
// target: what the symlink points to
|
|
// destination: where the symlink exists on disk
|
|
pub fn link(this: *Linker, global: bool) void {
|
|
const package_dir = this.buildTargetPackageDir();
|
|
var abs_dest_buf_remain = this.buildDestinationDir(global);
|
|
|
|
bun.assertWithLocation(this.bin.tag != .none, @src());
|
|
|
|
switch (this.bin.tag) {
|
|
.none => {},
|
|
.file => {
|
|
const target = this.bin.value.file.slice(this.string_buf);
|
|
if (target.len == 0) return;
|
|
|
|
// for normalizing `target`
|
|
const abs_target = path.joinAbsStringZ(package_dir, &.{target}, .auto);
|
|
|
|
const unscoped_package_name = Dependency.unscopedPackageName(this.package_name.slice());
|
|
@memcpy(abs_dest_buf_remain[0..unscoped_package_name.len], unscoped_package_name);
|
|
abs_dest_buf_remain = abs_dest_buf_remain[unscoped_package_name.len..];
|
|
abs_dest_buf_remain[0] = 0;
|
|
const abs_dest_len = @intFromPtr(abs_dest_buf_remain.ptr) - @intFromPtr(this.abs_dest_buf.ptr);
|
|
const abs_dest: [:0]const u8 = this.abs_dest_buf[0..abs_dest_len :0];
|
|
|
|
this.linkBinOrCreateShim(abs_target, abs_dest, global);
|
|
},
|
|
.named_file => {
|
|
const name = this.bin.value.named_file[0].slice(this.string_buf);
|
|
const normalized_name = normalizedBinName(name);
|
|
const target = this.bin.value.named_file[1].slice(this.string_buf);
|
|
if (normalized_name.len == 0 or target.len == 0) return;
|
|
|
|
// for normalizing `target`
|
|
const abs_target = path.joinAbsStringZ(package_dir, &.{target}, .auto);
|
|
|
|
@memcpy(abs_dest_buf_remain[0..normalized_name.len], normalized_name);
|
|
abs_dest_buf_remain = abs_dest_buf_remain[normalized_name.len..];
|
|
abs_dest_buf_remain[0] = 0;
|
|
const abs_dest_len = @intFromPtr(abs_dest_buf_remain.ptr) - @intFromPtr(this.abs_dest_buf.ptr);
|
|
const abs_dest: [:0]const u8 = this.abs_dest_buf[0..abs_dest_len :0];
|
|
|
|
this.linkBinOrCreateShim(abs_target, abs_dest, global);
|
|
},
|
|
.map => {
|
|
var i = this.bin.value.map.begin();
|
|
const end = this.bin.value.map.end();
|
|
|
|
const abs_dest_dir_end = abs_dest_buf_remain;
|
|
|
|
while (i < end) : (i += 2) {
|
|
const bin_dest = this.extern_string_buf[i].slice(this.string_buf);
|
|
const normalized_bin_dest = normalizedBinName(bin_dest);
|
|
const bin_target = this.extern_string_buf[i + 1].slice(this.string_buf);
|
|
if (bin_target.len == 0 or normalized_bin_dest.len == 0) continue;
|
|
|
|
const abs_target = path.joinAbsStringZ(package_dir, &.{bin_target}, .auto);
|
|
|
|
abs_dest_buf_remain = abs_dest_dir_end;
|
|
@memcpy(abs_dest_buf_remain[0..normalized_bin_dest.len], normalized_bin_dest);
|
|
abs_dest_buf_remain = abs_dest_buf_remain[normalized_bin_dest.len..];
|
|
abs_dest_buf_remain[0] = 0;
|
|
const abs_dest_len = @intFromPtr(abs_dest_buf_remain.ptr) - @intFromPtr(this.abs_dest_buf.ptr);
|
|
const abs_dest: [:0]const u8 = this.abs_dest_buf[0..abs_dest_len :0];
|
|
|
|
this.linkBinOrCreateShim(abs_target, abs_dest, global);
|
|
}
|
|
},
|
|
.dir => {
|
|
const target = this.bin.value.dir.slice(this.string_buf);
|
|
if (target.len == 0) return;
|
|
|
|
// for normalizing `target`
|
|
const abs_target_dir = path.joinAbsStringZ(package_dir, &.{target}, .auto);
|
|
|
|
var target_dir = bun.openDirAbsolute(abs_target_dir) catch |err| {
|
|
if (err == error.ENOENT) {
|
|
// https://github.com/npm/cli/blob/366c07e2f3cb9d1c6ddbd03e624a4d73fbd2676e/node_modules/bin-links/lib/link-gently.js#L43
|
|
// avoid erroring when the directory does not exist
|
|
return;
|
|
}
|
|
this.err = err;
|
|
return;
|
|
};
|
|
defer target_dir.close();
|
|
|
|
const abs_dest_dir_end = abs_dest_buf_remain;
|
|
|
|
var iter = target_dir.iterate();
|
|
while (iter.next() catch null) |entry| {
|
|
switch (entry.kind) {
|
|
.sym_link, .file => {
|
|
// `this.abs_target_buf` is available now because `path.joinAbsStringZ` copied everything into `parse_join_input_buffer`
|
|
const abs_target = path.joinAbsStringBufZ(abs_target_dir, this.abs_target_buf, &.{entry.name}, .auto);
|
|
|
|
abs_dest_buf_remain = abs_dest_dir_end;
|
|
@memcpy(abs_dest_buf_remain[0..entry.name.len], entry.name);
|
|
abs_dest_buf_remain = abs_dest_buf_remain[entry.name.len..];
|
|
abs_dest_buf_remain[0] = 0;
|
|
const abs_dest_len = @intFromPtr(abs_dest_buf_remain.ptr) - @intFromPtr(this.abs_dest_buf.ptr);
|
|
const abs_dest: [:0]const u8 = this.abs_dest_buf[0..abs_dest_len :0];
|
|
|
|
this.linkBinOrCreateShim(abs_target, abs_dest, global);
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn unlink(this: *Linker, global: bool) void {
|
|
const package_dir = this.buildTargetPackageDir();
|
|
var abs_dest_buf_remain = this.buildDestinationDir(global);
|
|
|
|
bun.assertWithLocation(this.bin.tag != .none, @src());
|
|
|
|
switch (this.bin.tag) {
|
|
.none => {},
|
|
.file => {
|
|
const unscoped_package_name = Dependency.unscopedPackageName(this.package_name.slice());
|
|
@memcpy(abs_dest_buf_remain[0..unscoped_package_name.len], unscoped_package_name);
|
|
abs_dest_buf_remain = abs_dest_buf_remain[unscoped_package_name.len..];
|
|
abs_dest_buf_remain[0] = 0;
|
|
const abs_dest_len = @intFromPtr(abs_dest_buf_remain.ptr) - @intFromPtr(this.abs_dest_buf.ptr);
|
|
const abs_dest: [:0]const u8 = this.abs_dest_buf[0..abs_dest_len :0];
|
|
|
|
unlinkBinOrShim(abs_dest);
|
|
},
|
|
.named_file => {
|
|
const name = this.bin.value.named_file[0].slice(this.string_buf);
|
|
const normalized_name = normalizedBinName(name);
|
|
if (normalized_name.len == 0) return;
|
|
|
|
@memcpy(abs_dest_buf_remain[0..normalized_name.len], normalized_name);
|
|
abs_dest_buf_remain = abs_dest_buf_remain[normalized_name.len..];
|
|
abs_dest_buf_remain[0] = 0;
|
|
const abs_dest_len = @intFromPtr(abs_dest_buf_remain.ptr) - @intFromPtr(this.abs_dest_buf.ptr);
|
|
const abs_dest: [:0]const u8 = this.abs_dest_buf[0..abs_dest_len :0];
|
|
|
|
unlinkBinOrShim(abs_dest);
|
|
},
|
|
.map => {
|
|
var i = this.bin.value.map.begin();
|
|
const end = this.bin.value.map.end();
|
|
|
|
const abs_dest_dir_end = abs_dest_buf_remain;
|
|
|
|
while (i < end) : (i += 2) {
|
|
const bin_dest = this.extern_string_buf[i].slice(this.string_buf);
|
|
const normalized_bin_dest = normalizedBinName(bin_dest);
|
|
if (normalized_bin_dest.len == 0) continue;
|
|
|
|
abs_dest_buf_remain = abs_dest_dir_end;
|
|
@memcpy(abs_dest_buf_remain[0..normalized_bin_dest.len], normalized_bin_dest);
|
|
abs_dest_buf_remain = abs_dest_buf_remain[normalized_bin_dest.len..];
|
|
abs_dest_buf_remain[0] = 0;
|
|
const abs_dest_len = @intFromPtr(abs_dest_buf_remain.ptr) - @intFromPtr(this.abs_dest_buf.ptr);
|
|
const abs_dest: [:0]const u8 = this.abs_dest_buf[0..abs_dest_len :0];
|
|
|
|
unlinkBinOrShim(abs_dest);
|
|
}
|
|
},
|
|
.dir => {
|
|
const target = this.bin.value.dir.slice(this.string_buf);
|
|
if (target.len == 0) return;
|
|
|
|
const abs_target_dir = path.joinAbsStringZ(package_dir, &.{target}, .auto);
|
|
|
|
var target_dir = bun.openDirAbsolute(abs_target_dir) catch |err| {
|
|
this.err = err;
|
|
return;
|
|
};
|
|
defer target_dir.close();
|
|
|
|
const abs_dest_dir_end = abs_dest_buf_remain;
|
|
|
|
var iter = target_dir.iterate();
|
|
while (iter.next() catch null) |entry| {
|
|
switch (entry.kind) {
|
|
.sym_link, .file => {
|
|
abs_dest_buf_remain = abs_dest_dir_end;
|
|
@memcpy(abs_dest_buf_remain[0..entry.name.len], entry.name);
|
|
abs_dest_buf_remain = abs_dest_buf_remain[entry.name.len..];
|
|
abs_dest_buf_remain[0] = 0;
|
|
const abs_dest_len = @intFromPtr(abs_dest_buf_remain.ptr) - @intFromPtr(this.abs_dest_buf.ptr);
|
|
const abs_dest: [:0]const u8 = this.abs_dest_buf[0..abs_dest_len :0];
|
|
|
|
unlinkBinOrShim(abs_dest);
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
};
|
|
};
|