aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-11-11 15:28:11 -0800
committerGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-12-16 19:18:51 -0800
commitd582e42d4d63aef921a263f192d229fe593ed5cb (patch)
tree8a692e996af9d2720cbaba09e0f346a782c3993b /src
parent6da14ae310d41110a00e1de9acefc94aea4fc26f (diff)
downloadbun-d582e42d4d63aef921a263f192d229fe593ed5cb.tar.gz
bun-d582e42d4d63aef921a263f192d229fe593ed5cb.tar.zst
bun-d582e42d4d63aef921a263f192d229fe593ed5cb.zip
wip
Diffstat (limited to 'src')
-rw-r--r--src/install/install.zig420
-rw-r--r--src/install/semver.zig31
2 files changed, 437 insertions, 14 deletions
diff --git a/src/install/install.zig b/src/install/install.zig
index 1e575015b..2efa4fa0b 100644
--- a/src/install/install.zig
+++ b/src/install/install.zig
@@ -23,12 +23,13 @@ const NodeModuleBundle = @import("../node_module_bundle.zig").NodeModuleBundle;
const DotEnv = @import("../env_loader.zig");
const which = @import("../which.zig").which;
const Run = @import("../bun_js.zig").Run;
+const NewBunQueue = @import("../bun_queue.zig").NewBunQueue;
var path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
var path_buf2: [std.fs.MAX_PATH_BYTES]u8 = undefined;
const URL = @import("../query_string_map.zig").URL;
-const URI = union(Tag) {
+pub const URI = union(Tag) {
local: string,
remote: URL,
@@ -40,15 +41,23 @@ const URI = union(Tag) {
const Semver = @import("./semver.zig");
-const Dependency = struct {
+pub const Dependency = struct {
name: string,
name_hash: u32,
version: Version,
pub const Version = union(Tag) {
pub const Tag = enum {
+ /// Semver range
npm,
+
+ /// NPM dist tag, e.g. "latest"
+ dist_tag,
+
+ /// URI to a .tgz or .tar.gz
tarball,
+
+ /// Local folder
folder,
/// TODO:
@@ -59,33 +68,416 @@ const Dependency = struct {
git,
/// TODO:
github,
+
+ pub fn isGitHubRepoPath(dependency: string) bool {
+ var slash_count: u8 = 0;
+
+ for (dependency) |c| {
+ slash_count += @as(u8, @boolToInt(c == '/'));
+ if (slash_count > 1 or c == '#') break;
+
+ // Must be alphanumeric
+ switch (c) {
+ '\\', '/', 'a'...'z', 'A'...'Z', '0'...'9', '%' => {},
+ else => return false,
+ }
+ }
+
+ return (slash_count == 1);
+ }
+
+ // this won't work for query string params
+ // i'll let someone file an issue before I add that
+ pub fn isTarball(dependency: string) bool {
+ return strings.endsWithComptime(dependency, ".tgz") or strings.endsWithComptime(dependency, ".tar.gz");
+ }
+
+ pub fn infer(dependency: string) Tag {
+ switch (dependency[0]) {
+ // npm package
+ '~', '0'...'9', '^', '*', '~', '|' => return Tag.npm,
+
+ // MIGHT be semver, might not be.
+ 'x', 'X' => {
+ if (dependecy.len == 1) {
+ return Tag.npm;
+ }
+
+ if (dependency[1] == '.') {
+ return Tag.npm;
+ }
+
+ return .dist_tag;
+ },
+
+ // git://, git@, git+ssh
+ 'g' => {
+ if (strings.eqlComptime(
+ dependency[0..@minimum("git://".len, dependency.len)],
+ "git://",
+ ) or strings.eqlComptime(
+ dependency[0..@minimum("git@".len, dependency.len)],
+ "git@",
+ ) or strings.eqlComptime(
+ dependency[0..@minimum("git+ssh".len, dependency.len)],
+ "git+ssh",
+ )) {
+ return .git;
+ }
+
+ if (strings.eqlComptime(
+ dependency[0..@minimum("github".len, dependency.len)],
+ "github",
+ ) or isGitHubRepoPath(dependency)) {
+ return .github;
+ }
+
+ return .dist_tag;
+ },
+
+ '/' => {
+ if (isTarball(dependency)) {
+ return .tarball;
+ }
+
+ return .folder;
+ },
+
+ // https://, http://
+ 'h' => {
+ if (isTarball(dependency)) {
+ return .tarball;
+ }
+
+ var remainder = dependency;
+ if (strings.eqlComptime(
+ remainder[0..@minimum("https://".len, remainder.len)],
+ "https://",
+ )) {
+ remainder = remainder["https://".len..];
+ }
+
+ if (strings.eqlComptime(
+ remainder[0..@minimum("http://".len, remainder.len)],
+ "http://",
+ )) {
+ remainder = remainder["http://".len..];
+ }
+
+ if (strings.eqlComptime(
+ remainder[0..@minimum("github".len, remainder.len)],
+ "github",
+ ) or isGitHubRepoPath(remainder)) {
+ return .github;
+ }
+
+ return .dist_tag;
+ },
+
+ // file://
+ 'f' => {
+ if (isTarball(dependency))
+ return .tarball;
+
+ if (strings.eqlComptime(
+ dependency[0..@minimum("file://".len, dependency.len)],
+ "file://",
+ )) {
+ return .folder;
+ }
+
+ if (isGitHubRepoPath(dependency)) {
+ return .github;
+ }
+
+ return .dist_tag;
+ },
+
+ // link://
+ 'l' => {
+ if (isTarball(dependency))
+ return .tarball;
+
+ if (strings.eqlComptime(
+ dependency[0..@minimum("link://".len, dependency.len)],
+ "link://",
+ )) {
+ return .symlink;
+ }
+
+ if (isGitHubRepoPath(dependency)) {
+ return .github;
+ }
+
+ return .dist_tag;
+ },
+
+ // workspace://
+ 'w' => {
+ if (strings.eqlComptime(
+ dependency[0..@minimum("workspace://".len, dependency.len)],
+ "workspace://",
+ )) {
+ return .workspace;
+ }
+
+ if (isTarball(dependency))
+ return .tarball;
+
+ if (isGitHubRepoPath(dependency)) {
+ return .github;
+ }
+
+ return .dist_tag;
+ },
+
+ else => {
+ if (isTarball(dependency))
+ return .tarball;
+
+ if (isGitHubRepoPath(dependency)) {
+ return .github;
+ }
+
+ return .dist_tag;
+ },
+ }
+ }
};
- version: Semver.Query.Group,
+ npm: Semver.Query.Group,
+ dist_tag: string,
tarball: URI,
folder: string,
+ /// Unsupported, but still parsed so an error can be thrown
symlink: void,
+ /// Unsupported, but still parsed so an error can be thrown
workspace: void,
+ /// Unsupported, but still parsed so an error can be thrown
git: void,
+ /// Unsupported, but still parsed so an error can be thrown
github: void,
};
pub const List = std.MultiArrayList(Dependency);
+
+ pub fn parse(allocator: *std.mem.Allocator, dependency_: string, log: *logger.Log) ?Version {
+ const dependency = std.mem.trimLeft(u8, dependency_, " \t\n\r");
+
+ if (dependency.len == 0) return null;
+
+ const tag = Tag.infer(dependency);
+
+ switch (tag) {
+ .npm => {
+ const version = Semver.Query.parse(allocator, dependency) catch |err| {
+ log.addErrorFmt(null, logger.Loc.Empty, allocator, "{s} parsing dependency \"{s}\"", .{ @errorName(err), dependency }) catch unreachable;
+ return null;
+ };
+
+ return Version{ .npm = version };
+ },
+ .dist_tag => {
+ return Version{ .dist_tag = dependency };
+ },
+ .tarball => {
+ if (strings.contains(dependency, "://")) {
+ if (strings.startsWith(dependency, "file://")) {
+ return Version{ .tarball = URI{ .local = dependency[7..] } };
+ } else if (strings.startsWith(dependency, "https://") or strings.startsWith(dependency, "http://")) {
+ return Version{ .tarball = URI{ .remote = dependency } };
+ } else {
+ log.addErrorFmt(null, logger.Loc.Empty, allocator, "invalid dependency \"{s}\"", .{dependency}) catch unreachable;
+ return null;
+ }
+ }
+
+ return Version{ .tarball = URI{ .local = dependency } };
+ },
+ .folder => {
+ if (strings.contains(dependency, "://")) {
+ if (strings.startsWith(dependency, "file://")) {
+ return Version{ .folder = dependency[7..] };
+ }
+
+ log.addErrorFmt(null, logger.Loc.Empty, allocator, "Unsupported protocol {s}", .{dependency}) catch unreachable;
+ return null;
+ }
+
+ return Version{ .folder = dependency };
+ },
+ .symlink, .workspace, .git, .github => {
+ log.addErrorFmt(null, logger.Loc.Empty, allocator, "Unsupported dependency type {s} for \"{s}\"", .{ @tagName(tag), dependency }) catch unreachable;
+ return null;
+ },
+ }
+ }
};
-const Package = struct {
- name: string,
- version: string,
- dependencies: Dependency.List,
- dev_dependencies: Dependency.List,
- peer_dependencies: Dependency.List,
- optional_dependencies: Dependency.List,
+pub const Package = struct {
+ name: string = "",
+ version: Semver.Version = Semver.Version{},
+ name_hash: u32 = 0,
+ dependencies: Dependency.List = Dependency.List{},
+ dev_dependencies: Dependency.List = Dependency.List{},
+ peer_dependencies: Dependency.List = Dependency.List{},
+ optional_dependencies: Dependency.List = Dependency.List{},
+
+ pub const Features = struct {
+ optional_dependencies: bool = false,
+ dev_dependencies: bool = false,
+ scripts: bool = false,
+ peer_dependencies: bool = true,
+ is_main: bool = false,
+ };
+
+ fn parseDependencyList(
+ allocator: *std.mem.Allocator,
+ log: *logger.Log,
+ expr: js_ast.Expr,
+ ) ?Dependency.List {
+ if (expr.data != .e_object) return null;
+
+ const properties = expr.data.e_object.properties;
+ if (properties.len == 0) return null;
+
+ var dependencies = Dependency.List{};
+ dependencies.ensureTotalCapacity(allocator, properties.len) catch @panic("OOM while parsing dependencies?");
+
+ for (properties) |prop| {
+ const name = prop.key.?.asString(allocator) orelse continue;
+ const value = prop.value.?.asString(allocator) orelse continue;
+
+ if (Dependency.parse(allocator, value, log)) |version| {
+ const dependency = Dependency{
+ .name = name,
+ .name_hash = std.hash.Murmur2_32.hash(name),
+ .version = version,
+ };
+
+ dependencies.appendAssumeCapacity(dependency);
+ }
+ }
+ return dependencies;
+ }
+
+ pub fn parse(
+ allocator: *std.mem.Allocator,
+ log: *logger.Log,
+ source: logger.Source,
+ comptime features: Features,
+ ) !Package {
+ var json = json_parser.ParseJSON(&source, log, allocator) catch |err| {
+ if (Output.enable_ansi_colors) {
+ log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true) catch {};
+ } else {
+ log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), false) catch {};
+ }
+
+ Output.panic("<r><red>{s}<r> parsing package.json for <b>\"{s}\"<r>", .{ @errorName(err), source.path.prettyDir() });
+ };
+
+ var package = Package{};
-
+ if (json.asProperty("name")) |name_q| {
+ if (name_q.expr.asString(allocator)) |name| {
+ package.name = name;
+ }
+ }
+
+ if (comptime !features.is_main) {
+ if (json.asProperty("version")) |version_q| {
+ if (version_q.expr.asString(allocator)) |version_str| {
+ const semver_version = Semver.Version.parse(allocator, version_str);
+
+ if (semver_version.valid) {
+ package.version = semver_version.version;
+ } else {
+ log.addErrorFmt(null, logger.Loc.Empty, allocator, "invalid version \"{s}\"", .{version_str}) catch unreachable;
+ }
+ }
+ }
+ }
+
+ if (json.asProperty("dependencies")) |dependencies_q| {
+ package.dependencies = parseDependencyList(allocator, dependencies_q.expr) orelse Dependency.List{};
+ }
+
+ if (comptime features.dev_dependencies) {
+ if (json.asProperty("devDependencies")) |dependencies_q| {
+ package.dev_dependencies = parseDependencyList(allocator, dependencies_q.expr) orelse Dependency.List{};
+ }
+ }
+
+ if (comptime features.optional_dependencies) {
+ if (json.asProperty("optionalDependencies")) |dependencies_q| {
+ package.optional_dependencies = parseDependencyList(allocator, dependencies_q.expr) orelse Dependency.List{};
+ }
+ }
+
+ if (comptime features.peer_dependencies) {
+ if (json.asProperty("peerDependencies")) |dependencies_q| {
+ package.peer_dependencies = parseDependencyList(allocator, dependencies_q.expr) orelse Dependency.List{};
+ }
+ }
+
+ if (comptime !features.is_main) {}
+
+ return package;
+ }
+};
+
+const Npm = struct {
+ pub const Registry = struct {
+ url: URL,
+ };
+
+ /// A package's "dependencies" field by their Semver version.
+ ///
+ /// Ordered by NPM registry version
+ ///
+ /// When the dependencies haven't changed between package versions,
+ /// Consider Dependency.List to be immutable. It may share a pointer with other entries in this map.
+ const DependencyMap = std.ArrayHashMap(Semver.Version, Dependency.List, Semver.Version.HashContext, true);
+
+ const ResolvedPackage = struct {
+ name: string,
+ name_hash: u32,
+
+ release_versions: DependencyMap,
+ pre_versions: DependencyMap,
+
+ pub fn parse(
+ allocator: *std.mem.Allocator,
+ log: *logger.Log,
+ json_buffer: []const u8,
+ expected_name: []const u8,
+ ) !?ResolvedPackage {
+ const source = logger.Source.initPathString(expected_name, json_buffer);
+ const json = json_parser.ParseJSON(&source, log, allocator) catch |err| {
+ return null;
+ };
+
+ if (json.asProperty("error")) |error_q| {
+ if (error_q.asString(allocator)) |err| {
+ log.addErrorFmt(&source, logger.Loc.Empty, allocator, "npm error: {s}", .{err}) catch unreachable;
+ return null;
+ }
+ }
+
+ if (json.asProperty("name")) |name_q| {
+ const name = name_q.expr.asString(allocator) orelse return null;
+
+ if (name != expected_name) {
+ Output.panic("<r>internal: <red>package name mismatch<r> expected \"{s}\" but received <b>\"{s}\"<r>", .{ expected_name, name });
+ return null;
+ }
+ }
+ }
+ };
};
-/// [Abbreviated NPM Package Version](https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object)
-const Registry = struct {
- url: URL,
+pub const Install = struct {
+ root_package: *Package = undefined,
};
diff --git a/src/install/semver.zig b/src/install/semver.zig
index 880357a09..115c73568 100644
--- a/src/install/semver.zig
+++ b/src/install/semver.zig
@@ -9,6 +9,27 @@ pub const Version = struct {
extra_tags: []const Tag = &[_]Tag{},
raw: strings.StringOrTinyString = strings.StringOrTinyString.init(""),
+ pub fn hash(this: Version) u32 {
+ var hasher = std.hash.Wyhash.init(0);
+ const triplet = [3]u32{ this.major, this.minor, this.patch };
+
+ hasher.update(std.mem.sliceAsBytes(&triplet));
+ const pre = this.tag.pre.slice();
+ const build = this.tag.build.slice();
+
+ if (pre.len > 0) {
+ hasher.update("+");
+ hasher.update(pre);
+ }
+
+ if (build.len > 0) {
+ hasher.update("-");
+ hasher.update(build);
+ }
+
+ return @truncate(u32, hasher.final());
+ }
+
pub fn format(self: Version, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void {
try std.fmt.format(writer, "{d}.{d}.{d}", .{ self.major, self.minor, self.patch });
@@ -39,6 +60,16 @@ pub const Version = struct {
return lhs.major == rhs.major and lhs.minor == rhs.minor and lhs.patch == rhs.patch and rhs.tag.eql(lhs.tag);
}
+ pub const HashContext = struct {
+ pub fn hash(this: @This(), lhs: Version) u32 {
+ return lhs.hash();
+ }
+
+ pub fn eql(this: @This(), lhs: Version, rhs: Version) bool {
+ return lhs.eql(rhs);
+ }
+ };
+
pub fn order(lhs: Version, rhs: Version) std.math.Order {
if (lhs.major < rhs.major) return .lt;
if (lhs.major > rhs.major) return .gt;