@foundry-rs/forge 1.4.4-nightly.20251030.87a024e → 1.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin.mjs +119 -0
- package/const.mjs +87 -0
- package/package.json +14 -10
- package/postinstall.mjs +267 -0
- package/bin/forge.mjs +0 -54
- package/dist/install.mjs +0 -171
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
# Forge
|
|
2
2
|
|
|
3
3
|
Forge is a command-line tool that ships with Foundry. Forge tests, builds, and deploys your smart contracts.
|
|
4
|
-
|
|
4
|
+
The forge binary can be used both within and outside of a Foundry project.
|
package/bin.mjs
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { BINARY_NAME, colors, KNOWN_TOOLS, PLATFORM_SPECIFIC_PACKAGE_NAME, resolveTargetTool } from '#const.mjs'
|
|
4
|
+
import * as NodeChildProcess from 'node:child_process'
|
|
5
|
+
import * as NodeFS from 'node:fs'
|
|
6
|
+
import * as NodeModule from 'node:module'
|
|
7
|
+
import * as NodePath from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import('#const.mjs').Tool} Tool
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const require = NodeModule.createRequire(import.meta.url)
|
|
15
|
+
const __dirname = NodePath.dirname(fileURLToPath(import.meta.url))
|
|
16
|
+
|
|
17
|
+
const targetTool = resolveTool()
|
|
18
|
+
process.env.TARGET_TOOL ??= targetTool
|
|
19
|
+
|
|
20
|
+
const binaryName = BINARY_NAME(targetTool)
|
|
21
|
+
const platformPackage = PLATFORM_SPECIFIC_PACKAGE_NAME(targetTool)
|
|
22
|
+
|
|
23
|
+
if (!platformPackage) {
|
|
24
|
+
console.error(colors.red, 'Platform not supported!')
|
|
25
|
+
console.error(colors.reset)
|
|
26
|
+
console.error(colors.yellow, `Platform: ${process.platform}, Architecture: ${process.arch}`)
|
|
27
|
+
console.error(colors.reset)
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
NodeChildProcess.spawn(selectBinaryPath(), process.argv.slice(2), { stdio: 'inherit' })
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Determines which tool wrapper is executing.
|
|
35
|
+
* @returns {Tool}
|
|
36
|
+
*/
|
|
37
|
+
function resolveTool() {
|
|
38
|
+
const candidates = [
|
|
39
|
+
process.env.TARGET_TOOL,
|
|
40
|
+
toolFromPackageName(process.env.npm_package_name),
|
|
41
|
+
toolFromLocalPackage(),
|
|
42
|
+
toolFromPath()
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
for (const candidate of candidates) {
|
|
46
|
+
if (!candidate) continue
|
|
47
|
+
try {
|
|
48
|
+
return resolveTargetTool(candidate)
|
|
49
|
+
} catch {
|
|
50
|
+
// try next
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
throw new Error('TARGET_TOOL must be set to one of: ' + KNOWN_TOOLS.join(', '))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Attempts to read the tool name from the nearest package.json.
|
|
59
|
+
* @returns {Tool | undefined}
|
|
60
|
+
*/
|
|
61
|
+
function toolFromLocalPackage() {
|
|
62
|
+
try {
|
|
63
|
+
const packageJsonPath = NodePath.join(__dirname, 'package.json')
|
|
64
|
+
if (!NodeFS.existsSync(packageJsonPath)) return undefined
|
|
65
|
+
const pkg = require(packageJsonPath)
|
|
66
|
+
return toolFromPackageName(pkg?.name)
|
|
67
|
+
} catch {
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Extracts the tool name from an @foundry-rs scoped package identifier.
|
|
74
|
+
* @param {unknown} name
|
|
75
|
+
* @returns {Tool | undefined}
|
|
76
|
+
*/
|
|
77
|
+
function toolFromPackageName(name) {
|
|
78
|
+
if (typeof name !== 'string') return undefined
|
|
79
|
+
const match = name.match(/^@foundry-rs\/(forge|cast|anvil|chisel)(?:$|-)/)
|
|
80
|
+
return match ? /** @type {Tool} */ (match[1]) : undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Walks up the directory tree to infer the tool name from the folder structure.
|
|
85
|
+
* @returns {Tool | undefined}
|
|
86
|
+
*/
|
|
87
|
+
function toolFromPath() {
|
|
88
|
+
const segments = NodePath.resolve(__dirname).split(NodePath.sep)
|
|
89
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
90
|
+
const candidate = segments[i]
|
|
91
|
+
if (isTool(candidate)) return candidate
|
|
92
|
+
}
|
|
93
|
+
return undefined
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Type guard verifying a candidate string is a known tool.
|
|
98
|
+
* @param {string | undefined} candidate
|
|
99
|
+
* @returns {candidate is Tool}
|
|
100
|
+
*/
|
|
101
|
+
function isTool(candidate) {
|
|
102
|
+
if (typeof candidate !== 'string') return false
|
|
103
|
+
return KNOWN_TOOLS.includes(/** @type {Tool} */ (candidate))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Determines the executable file path for the current platform.
|
|
108
|
+
* @returns {string}
|
|
109
|
+
*/
|
|
110
|
+
function selectBinaryPath() {
|
|
111
|
+
try {
|
|
112
|
+
const candidate = require.resolve(`${platformPackage}/bin/${binaryName}`)
|
|
113
|
+
if (NodeFS.existsSync(candidate)) return candidate
|
|
114
|
+
} catch {
|
|
115
|
+
// fall through to dist/ binary
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return NodePath.join(__dirname, '..', 'dist', binaryName)
|
|
119
|
+
}
|
package/const.mjs
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as NodePath from 'node:path'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {'amd64' | 'arm64'} Arch
|
|
5
|
+
* @typedef {'linux' | 'darwin' | 'win32'} Platform
|
|
6
|
+
* @typedef {'forge' | 'cast' | 'anvil' | 'chisel'} Tool
|
|
7
|
+
* @typedef {'debug' | 'release' | 'maxperf'} Profile
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** @type {readonly Tool[]} */
|
|
11
|
+
export const KNOWN_TOOLS = Object.freeze(['forge', 'cast', 'anvil', 'chisel'])
|
|
12
|
+
|
|
13
|
+
const TOOL_SET = new Set(KNOWN_TOOLS)
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string | undefined} [raw]
|
|
17
|
+
* @returns {Tool}
|
|
18
|
+
*
|
|
19
|
+
* could be process.argv[2]
|
|
20
|
+
*/
|
|
21
|
+
export function resolveTargetTool(raw = process.env.TARGET_TOOL || process.argv[2]) {
|
|
22
|
+
const value = typeof raw === 'string' ? raw.trim() : ''
|
|
23
|
+
if (!value)
|
|
24
|
+
throw new Error('TARGET_TOOL must be set to one of: ' + KNOWN_TOOLS.join(', '))
|
|
25
|
+
if (value !== NodePath.basename(value) || value.includes('..') || value.includes('/') || value.includes('\\'))
|
|
26
|
+
throw new Error('TARGET_TOOL contains invalid path segments')
|
|
27
|
+
// @ts-expect-error _
|
|
28
|
+
if (!TOOL_SET.has(value))
|
|
29
|
+
throw new Error(`TARGET_TOOL "${value}" is not supported. Expected: ${KNOWN_TOOLS.join(', ')}`)
|
|
30
|
+
return /** @type {Tool} */ (value)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getRegistryUrl() {
|
|
34
|
+
// Prefer npm's configured registry (works with Verdaccio and custom registries)
|
|
35
|
+
// Fallback to REGISTRY_URL for tests/dev, then npmjs
|
|
36
|
+
return (
|
|
37
|
+
process.env.npm_config_registry
|
|
38
|
+
|| process.env.REGISTRY_URL
|
|
39
|
+
|| 'https://registry.npmjs.org'
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {Tool} tool
|
|
45
|
+
* @returns {Record<Platform, Record<string, string>>}
|
|
46
|
+
*/
|
|
47
|
+
export const BINARY_DISTRIBUTION_PACKAGES = tool => ({
|
|
48
|
+
darwin: {
|
|
49
|
+
x64: `@foundry-rs/${tool}-darwin-amd64`,
|
|
50
|
+
arm64: `@foundry-rs/${tool}-darwin-arm64`
|
|
51
|
+
},
|
|
52
|
+
linux: {
|
|
53
|
+
x64: `@foundry-rs/${tool}-linux-amd64`,
|
|
54
|
+
arm64: `@foundry-rs/${tool}-linux-arm64`
|
|
55
|
+
},
|
|
56
|
+
win32: {
|
|
57
|
+
x64: `@foundry-rs/${tool}-win32-amd64`
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {Tool} tool
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
export const BINARY_NAME = tool => process.platform === 'win32' ? `${tool}.exe` : tool
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {Tool} tool
|
|
69
|
+
* @returns {string | undefined}
|
|
70
|
+
*/
|
|
71
|
+
export const PLATFORM_SPECIFIC_PACKAGE_NAME = tool => {
|
|
72
|
+
// @ts-ignore
|
|
73
|
+
const platformPackages = BINARY_DISTRIBUTION_PACKAGES(tool)[process.platform]
|
|
74
|
+
if (!platformPackages) return undefined
|
|
75
|
+
return platformPackages?.[process.arch]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const colors = {
|
|
79
|
+
red: '\x1b[31m',
|
|
80
|
+
green: '\x1b[32m',
|
|
81
|
+
yellow: '\x1b[33m',
|
|
82
|
+
blue: '\x1b[34m',
|
|
83
|
+
magenta: '\x1b[35m',
|
|
84
|
+
cyan: '\x1b[36m',
|
|
85
|
+
white: '\x1b[37m',
|
|
86
|
+
reset: '\x1b[0m'
|
|
87
|
+
}
|
package/package.json
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foundry-rs/forge",
|
|
3
|
-
"version": "1.4.4
|
|
3
|
+
"version": "1.4.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"homepage": "https://getfoundry.sh/forge",
|
|
6
6
|
"description": "Fast and flexible Ethereum testing framework",
|
|
7
7
|
"bin": {
|
|
8
|
-
"forge": "./bin
|
|
8
|
+
"forge": "./bin.mjs"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"bin",
|
|
12
|
-
"
|
|
11
|
+
"bin.mjs",
|
|
12
|
+
"const.mjs",
|
|
13
|
+
"postinstall.mjs"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
15
|
-
"postinstall": "node ./
|
|
16
|
+
"postinstall": "TARGET_TOOL=forge node ./postinstall.mjs"
|
|
16
17
|
},
|
|
17
18
|
"optionalDependencies": {
|
|
18
|
-
"@foundry-rs/forge-darwin-arm64": "1.4.4
|
|
19
|
-
"@foundry-rs/forge-darwin-amd64": "1.4.4
|
|
20
|
-
"@foundry-rs/forge-linux-arm64": "1.4.4
|
|
21
|
-
"@foundry-rs/forge-linux-amd64": "1.4.4
|
|
22
|
-
"@foundry-rs/forge-win32-amd64": "1.4.4
|
|
19
|
+
"@foundry-rs/forge-darwin-arm64": "1.4.4",
|
|
20
|
+
"@foundry-rs/forge-darwin-amd64": "1.4.4",
|
|
21
|
+
"@foundry-rs/forge-linux-arm64": "1.4.4",
|
|
22
|
+
"@foundry-rs/forge-linux-amd64": "1.4.4",
|
|
23
|
+
"@foundry-rs/forge-win32-amd64": "1.4.4"
|
|
23
24
|
},
|
|
24
25
|
"publishConfig": {
|
|
25
26
|
"access": "public",
|
|
@@ -30,5 +31,8 @@
|
|
|
30
31
|
"repository": {
|
|
31
32
|
"directory": "npm",
|
|
32
33
|
"url": "https://github.com/foundry-rs/foundry"
|
|
34
|
+
},
|
|
35
|
+
"imports": {
|
|
36
|
+
"#const.mjs": "./const.mjs"
|
|
33
37
|
}
|
|
34
38
|
}
|
package/postinstall.mjs
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/install.mjs
|
|
4
|
+
import * as NodeCrypto from "node:crypto";
|
|
5
|
+
import * as NodeFS from "node:fs";
|
|
6
|
+
import * as NodeHttp from "node:http";
|
|
7
|
+
import * as NodeHttps from "node:https";
|
|
8
|
+
import * as NodeModule from "node:module";
|
|
9
|
+
import * as NodePath2 from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import * as NodeZlib from "node:zlib";
|
|
12
|
+
|
|
13
|
+
// src/const.mjs
|
|
14
|
+
import * as NodePath from "node:path";
|
|
15
|
+
var KNOWN_TOOLS = Object.freeze(["forge", "cast", "anvil", "chisel"]);
|
|
16
|
+
var TOOL_SET = new Set(KNOWN_TOOLS);
|
|
17
|
+
function resolveTargetTool(raw = process.env.TARGET_TOOL || process.argv[2]) {
|
|
18
|
+
const value = typeof raw === "string" ? raw.trim() : "";
|
|
19
|
+
if (!value)
|
|
20
|
+
throw new Error("TARGET_TOOL must be set to one of: " + KNOWN_TOOLS.join(", "));
|
|
21
|
+
if (value !== NodePath.basename(value) || value.includes("..") || value.includes("/") || value.includes("\\"))
|
|
22
|
+
throw new Error("TARGET_TOOL contains invalid path segments");
|
|
23
|
+
if (!TOOL_SET.has(value))
|
|
24
|
+
throw new Error(`TARGET_TOOL "${value}" is not supported. Expected: ${KNOWN_TOOLS.join(", ")}`);
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function getRegistryUrl() {
|
|
28
|
+
return process.env.npm_config_registry || process.env.REGISTRY_URL || "https://registry.npmjs.org";
|
|
29
|
+
}
|
|
30
|
+
var BINARY_DISTRIBUTION_PACKAGES = (tool) => ({
|
|
31
|
+
darwin: {
|
|
32
|
+
x64: `@foundry-rs/${tool}-darwin-amd64`,
|
|
33
|
+
arm64: `@foundry-rs/${tool}-darwin-arm64`
|
|
34
|
+
},
|
|
35
|
+
linux: {
|
|
36
|
+
x64: `@foundry-rs/${tool}-linux-amd64`,
|
|
37
|
+
arm64: `@foundry-rs/${tool}-linux-arm64`
|
|
38
|
+
},
|
|
39
|
+
win32: {
|
|
40
|
+
x64: `@foundry-rs/${tool}-win32-amd64`
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
var BINARY_NAME = (tool) => process.platform === "win32" ? `${tool}.exe` : tool;
|
|
44
|
+
var PLATFORM_SPECIFIC_PACKAGE_NAME = (tool) => {
|
|
45
|
+
const platformPackages = BINARY_DISTRIBUTION_PACKAGES(tool)[process.platform];
|
|
46
|
+
if (!platformPackages)
|
|
47
|
+
return;
|
|
48
|
+
return platformPackages?.[process.arch];
|
|
49
|
+
};
|
|
50
|
+
var colors = {
|
|
51
|
+
red: "\x1B[31m",
|
|
52
|
+
green: "\x1B[32m",
|
|
53
|
+
yellow: "\x1B[33m",
|
|
54
|
+
blue: "\x1B[34m",
|
|
55
|
+
magenta: "\x1B[35m",
|
|
56
|
+
cyan: "\x1B[36m",
|
|
57
|
+
white: "\x1B[37m",
|
|
58
|
+
reset: "\x1B[0m"
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/install.mjs
|
|
62
|
+
var __dirname2 = NodePath2.dirname(fileURLToPath(import.meta.url));
|
|
63
|
+
var targetTool = resolveTargetTool();
|
|
64
|
+
var binaryName = BINARY_NAME(targetTool);
|
|
65
|
+
var fallbackBinaryPath = NodePath2.join(__dirname2, binaryName);
|
|
66
|
+
var platformSpecificPackageName = PLATFORM_SPECIFIC_PACKAGE_NAME(targetTool);
|
|
67
|
+
var expectedTarEntryPath = `package/bin/${binaryName}`;
|
|
68
|
+
if (NodePath2.relative(__dirname2, fallbackBinaryPath).startsWith(".."))
|
|
69
|
+
throw new Error("Resolved binary path escapes package directory");
|
|
70
|
+
if (!platformSpecificPackageName)
|
|
71
|
+
throw new Error("Platform not supported!");
|
|
72
|
+
var require2 = NodeModule.createRequire(import.meta.url);
|
|
73
|
+
function ensureSecureUrl(urlString, purpose) {
|
|
74
|
+
try {
|
|
75
|
+
const url = new URL(urlString);
|
|
76
|
+
if (url.protocol === "http:") {
|
|
77
|
+
const allowInsecure = process.env.ALLOW_INSECURE_REGISTRY === "true";
|
|
78
|
+
if (!["localhost", "127.0.0.1", "::1"].includes(url.hostname) && !allowInsecure) {
|
|
79
|
+
throw new Error(`Refusing to use insecure HTTP for ${purpose}: ${urlString}. ` + `Set ALLOW_INSECURE_REGISTRY=true to override (not recommended).`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch {}
|
|
83
|
+
}
|
|
84
|
+
var MAX_REDIRECTS = 10;
|
|
85
|
+
var REQUEST_TIMEOUT = 30000;
|
|
86
|
+
var MAX_METADATA_BYTES = 5 * 1024 * 1024;
|
|
87
|
+
var MAX_TARBALL_BYTES = 200 * 1024 * 1024;
|
|
88
|
+
var MAX_BINARY_BYTES = 500 * 1024 * 1024;
|
|
89
|
+
function makeRequest(url, options = {}) {
|
|
90
|
+
const {
|
|
91
|
+
parentSignal,
|
|
92
|
+
redirectDepth = 0,
|
|
93
|
+
visited = new Set,
|
|
94
|
+
maxBytes,
|
|
95
|
+
collect = true,
|
|
96
|
+
onChunk
|
|
97
|
+
} = options;
|
|
98
|
+
if (redirectDepth > MAX_REDIRECTS)
|
|
99
|
+
throw new Error("Maximum redirect depth exceeded");
|
|
100
|
+
if (visited.has(url))
|
|
101
|
+
throw new Error("Circular redirect detected");
|
|
102
|
+
visited.add(url);
|
|
103
|
+
ensureSecureUrl(url, "HTTP request");
|
|
104
|
+
const controller = new AbortController;
|
|
105
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
|
|
106
|
+
const signal = parentSignal ? AbortSignal.any([parentSignal, controller.signal]) : controller.signal;
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
const client = url.startsWith("https:") ? NodeHttps : NodeHttp;
|
|
109
|
+
const request = client.get(url, { signal }, (response) => {
|
|
110
|
+
const finish = (error, value = undefined) => {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
if (error)
|
|
113
|
+
reject(error);
|
|
114
|
+
else
|
|
115
|
+
resolve(value);
|
|
116
|
+
};
|
|
117
|
+
if (response?.statusCode && response.statusCode >= 200 && response.statusCode < 300) {
|
|
118
|
+
let totalBytes = 0;
|
|
119
|
+
const chunks = collect ? [] : undefined;
|
|
120
|
+
response.on("data", (chunk) => {
|
|
121
|
+
totalBytes += chunk.length;
|
|
122
|
+
if (maxBytes && totalBytes > maxBytes) {
|
|
123
|
+
response.destroy(new Error("Response exceeded maximum allowed size"));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (chunks)
|
|
127
|
+
chunks.push(chunk);
|
|
128
|
+
if (onChunk) {
|
|
129
|
+
try {
|
|
130
|
+
onChunk(chunk, response);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
response.destroy(error instanceof Error ? error : new Error(`Error occurred: ${error}`));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
response.on("end", () => {
|
|
137
|
+
finish(null, chunks ? Buffer.concat(chunks) : undefined);
|
|
138
|
+
});
|
|
139
|
+
response.on("error", finish);
|
|
140
|
+
} else if (response?.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
const nextUrl = new URL(response.headers.location, url).href;
|
|
143
|
+
return makeRequest(nextUrl, {
|
|
144
|
+
parentSignal: signal,
|
|
145
|
+
redirectDepth: redirectDepth + 1,
|
|
146
|
+
visited,
|
|
147
|
+
maxBytes,
|
|
148
|
+
collect,
|
|
149
|
+
onChunk
|
|
150
|
+
}).then(resolve, reject);
|
|
151
|
+
} else {
|
|
152
|
+
finish(new Error(`Package registry responded with status code ${response?.statusCode ?? "(none)"} when downloading the package.`));
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
request.on("error", (error) => {
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
reject(error);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function extractFileFromTarball(tarballBuffer, filepath) {
|
|
162
|
+
let offset = 0;
|
|
163
|
+
while (offset < tarballBuffer.length) {
|
|
164
|
+
const header = tarballBuffer.subarray(offset, offset + 512);
|
|
165
|
+
offset += 512;
|
|
166
|
+
const fileName = header.toString("utf-8", 0, 100).replace(/\0.*/g, "");
|
|
167
|
+
const fileSize = Number.parseInt(header.toString("utf-8", 124, 136).replace(/\0.*/g, ""), 8);
|
|
168
|
+
if (fileName === filepath) {
|
|
169
|
+
if (!Number.isFinite(fileSize) || Number.isNaN(fileSize) || fileSize < 0)
|
|
170
|
+
throw new Error(`Invalid size for ${filepath} in tarball`);
|
|
171
|
+
if (fileSize > MAX_BINARY_BYTES)
|
|
172
|
+
throw new Error(`Binary size for ${filepath} exceeds maximum allowed threshold`);
|
|
173
|
+
return tarballBuffer.subarray(offset, offset + fileSize);
|
|
174
|
+
}
|
|
175
|
+
offset = offset + fileSize + 511 & ~511;
|
|
176
|
+
}
|
|
177
|
+
throw new Error(`File ${filepath} not found in tarball`);
|
|
178
|
+
}
|
|
179
|
+
async function downloadBinaryFromRegistry() {
|
|
180
|
+
if (!platformSpecificPackageName)
|
|
181
|
+
throw new Error("Platform-specific package name is not defined");
|
|
182
|
+
const registryUrl = getRegistryUrl().replace(/\/$/, "");
|
|
183
|
+
ensureSecureUrl(registryUrl, "registry URL");
|
|
184
|
+
const encodedName = platformSpecificPackageName.startsWith("@") ? encodeURIComponent(platformSpecificPackageName) : platformSpecificPackageName;
|
|
185
|
+
let desiredVersion;
|
|
186
|
+
try {
|
|
187
|
+
const pkgJsonPath = NodePath2.join(__dirname2, "..", "package.json");
|
|
188
|
+
const pkgJson = JSON.parse(NodeFS.readFileSync(pkgJsonPath, "utf8"));
|
|
189
|
+
desiredVersion = pkgJson?.optionalDependencies[platformSpecificPackageName] || pkgJson?.version;
|
|
190
|
+
} catch {}
|
|
191
|
+
const metaUrl = `${registryUrl}/${encodedName}`;
|
|
192
|
+
const metaBuffer = await makeRequest(metaUrl, { maxBytes: MAX_METADATA_BYTES });
|
|
193
|
+
if (!metaBuffer)
|
|
194
|
+
throw new Error("Failed to download package metadata");
|
|
195
|
+
const metadata = JSON.parse(metaBuffer.toString("utf8"));
|
|
196
|
+
const version = desiredVersion || metadata?.["dist-tags"]?.latest;
|
|
197
|
+
const versionMeta = metadata?.versions?.[version];
|
|
198
|
+
const dist = versionMeta?.dist;
|
|
199
|
+
if (!dist?.tarball) {
|
|
200
|
+
throw new Error(`Could not find tarball for ${platformSpecificPackageName}@${version} from ${metaUrl}`);
|
|
201
|
+
}
|
|
202
|
+
ensureSecureUrl(dist.tarball, "tarball URL");
|
|
203
|
+
console.info(colors.green, `Downloading binary from:
|
|
204
|
+
`, dist.tarball, `
|
|
205
|
+
`, colors.reset);
|
|
206
|
+
const integrity = typeof dist.integrity === "string" ? dist.integrity : "";
|
|
207
|
+
const sriMatch = integrity.match(/^([a-z0-9]+)-([A-Za-z0-9+/=]+)$/i);
|
|
208
|
+
const allowedSRIAlgorithms = new Set(["sha512", "sha256", "sha1"]);
|
|
209
|
+
const sriAlgo = sriMatch && allowedSRIAlgorithms.has(sriMatch[1].toLowerCase()) ? sriMatch[1].toLowerCase() : undefined;
|
|
210
|
+
const expectedSri = sriAlgo ? sriMatch?.[2] : undefined;
|
|
211
|
+
const sriHasher = sriAlgo ? NodeCrypto.createHash(sriAlgo) : undefined;
|
|
212
|
+
const expectedSha1Hex = typeof dist.shasum === "string" && dist.shasum.length === 40 ? dist.shasum.toLowerCase() : undefined;
|
|
213
|
+
const sha1Hasher = expectedSha1Hex ? NodeCrypto.createHash("sha1") : undefined;
|
|
214
|
+
const tarballDownloadBuffer = await makeRequest(dist.tarball, {
|
|
215
|
+
maxBytes: MAX_TARBALL_BYTES,
|
|
216
|
+
onChunk: (chunk) => {
|
|
217
|
+
sriHasher?.update(chunk);
|
|
218
|
+
sha1Hasher?.update(chunk);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
if (!tarballDownloadBuffer)
|
|
222
|
+
throw new Error("Failed to download tarball contents");
|
|
223
|
+
let verified = false;
|
|
224
|
+
if (sriHasher && expectedSri) {
|
|
225
|
+
const actual = sriHasher.digest("base64");
|
|
226
|
+
if (expectedSri !== actual) {
|
|
227
|
+
throw new Error(`Downloaded tarball failed integrity check (${sriAlgo} mismatch)`);
|
|
228
|
+
}
|
|
229
|
+
verified = true;
|
|
230
|
+
}
|
|
231
|
+
if (!verified && sha1Hasher && expectedSha1Hex) {
|
|
232
|
+
const actualSha1Hex = sha1Hasher.digest("hex");
|
|
233
|
+
if (expectedSha1Hex !== actualSha1Hex) {
|
|
234
|
+
throw new Error("Downloaded tarball failed integrity check (sha1 shasum mismatch)");
|
|
235
|
+
}
|
|
236
|
+
verified = true;
|
|
237
|
+
}
|
|
238
|
+
if (!verified) {
|
|
239
|
+
const allowNoIntegrity = process.env.ALLOW_NO_INTEGRITY === "true" || process.env.ALLOW_UNVERIFIED_TARBALL === "true";
|
|
240
|
+
if (!allowNoIntegrity) {
|
|
241
|
+
throw new Error("No integrity metadata found for downloaded tarball. " + "Set ALLOW_NO_INTEGRITY=true to bypass (not recommended).");
|
|
242
|
+
}
|
|
243
|
+
console.warn(colors.yellow, "Warning: proceeding without integrity verification (explicitly allowed).", colors.reset);
|
|
244
|
+
}
|
|
245
|
+
const tarballBuffer = NodeZlib.gunzipSync(tarballDownloadBuffer);
|
|
246
|
+
NodeFS.writeFileSync(fallbackBinaryPath, extractFileFromTarball(tarballBuffer, expectedTarEntryPath), { mode: 493 });
|
|
247
|
+
}
|
|
248
|
+
function isPlatformSpecificPackageInstalled() {
|
|
249
|
+
try {
|
|
250
|
+
require2.resolve(`${platformSpecificPackageName}/bin/${binaryName}`);
|
|
251
|
+
return true;
|
|
252
|
+
} catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (!isPlatformSpecificPackageInstalled()) {
|
|
257
|
+
console.log("Platform specific package not found. Will manually download binary.");
|
|
258
|
+
downloadBinaryFromRegistry().catch((error) => {
|
|
259
|
+
console.error(colors.red, "Failed to download binary:", error, colors.reset);
|
|
260
|
+
process.exitCode = 1;
|
|
261
|
+
});
|
|
262
|
+
} else {
|
|
263
|
+
console.log("Platform specific package already installed. Skipping manual download.");
|
|
264
|
+
}
|
|
265
|
+
export {
|
|
266
|
+
makeRequest
|
|
267
|
+
};
|
package/bin/forge.mjs
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import * as NodeModule from "node:module";
|
|
3
|
-
import * as NodeChildProcess from "node:child_process";
|
|
4
|
-
import * as NodeFS from "node:fs";
|
|
5
|
-
import * as NodePath from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
|
|
8
|
-
//#region src/const.ts
|
|
9
|
-
const BINARY_DISTRIBUTION_PACKAGES = {
|
|
10
|
-
darwin: {
|
|
11
|
-
x64: "@foundry-rs/forge-darwin-amd64",
|
|
12
|
-
arm64: "@foundry-rs/forge-darwin-arm64"
|
|
13
|
-
},
|
|
14
|
-
linux: {
|
|
15
|
-
x64: "@foundry-rs/forge-linux-amd64",
|
|
16
|
-
arm64: "@foundry-rs/forge-linux-arm64"
|
|
17
|
-
},
|
|
18
|
-
win32: { x64: "@foundry-rs/forge-win32-amd64" }
|
|
19
|
-
};
|
|
20
|
-
const BINARY_NAME = process.platform === "win32" ? "forge.exe" : "forge";
|
|
21
|
-
const PLATFORM_SPECIFIC_PACKAGE_NAME = BINARY_DISTRIBUTION_PACKAGES[process.platform][process.arch];
|
|
22
|
-
const colors = {
|
|
23
|
-
red: "\x1B[31m",
|
|
24
|
-
green: "\x1B[32m",
|
|
25
|
-
yellow: "\x1B[33m",
|
|
26
|
-
blue: "\x1B[34m",
|
|
27
|
-
magenta: "\x1B[35m",
|
|
28
|
-
cyan: "\x1B[36m",
|
|
29
|
-
white: "\x1B[37m",
|
|
30
|
-
reset: "\x1B[0m"
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
//#endregion
|
|
34
|
-
//#region src/forge.ts
|
|
35
|
-
const require = NodeModule.createRequire(import.meta.url);
|
|
36
|
-
const __dirname = NodePath.dirname(fileURLToPath(import.meta.url));
|
|
37
|
-
function getBinaryPath() {
|
|
38
|
-
try {
|
|
39
|
-
const binaryPath = require.resolve(`${PLATFORM_SPECIFIC_PACKAGE_NAME}/bin/${BINARY_NAME}`);
|
|
40
|
-
if (NodeFS.existsSync(binaryPath)) return binaryPath;
|
|
41
|
-
} catch {
|
|
42
|
-
return NodePath.join(__dirname, "..", "dist", BINARY_NAME);
|
|
43
|
-
}
|
|
44
|
-
console.error(colors.red, `Platform-specific package ${PLATFORM_SPECIFIC_PACKAGE_NAME} not found.`);
|
|
45
|
-
console.error(colors.yellow, "This usually means the installation failed or your platform is not supported.");
|
|
46
|
-
console.error(colors.reset);
|
|
47
|
-
console.error(colors.yellow, `Platform: ${process.platform}, Architecture: ${process.arch}`);
|
|
48
|
-
console.error(colors.reset);
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
NodeChildProcess.spawn(getBinaryPath(), process.argv.slice(2), { stdio: "inherit" });
|
|
52
|
-
|
|
53
|
-
//#endregion
|
|
54
|
-
export { };
|
package/dist/install.mjs
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import * as NodeModule from "node:module";
|
|
3
|
-
import * as NodeCrypto from "node:crypto";
|
|
4
|
-
import * as NodeFS from "node:fs";
|
|
5
|
-
import * as NodeHttp from "node:http";
|
|
6
|
-
import * as NodeHttps from "node:https";
|
|
7
|
-
import * as NodePath from "node:path";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
import * as NodeZlib from "node:zlib";
|
|
10
|
-
|
|
11
|
-
//#region src/const.ts
|
|
12
|
-
function getRegistryUrl() {
|
|
13
|
-
return process.env.npm_config_registry || process.env.REGISTRY_URL || "https://registry.npmjs.org";
|
|
14
|
-
}
|
|
15
|
-
const BINARY_DISTRIBUTION_PACKAGES = {
|
|
16
|
-
darwin: {
|
|
17
|
-
x64: "@foundry-rs/forge-darwin-amd64",
|
|
18
|
-
arm64: "@foundry-rs/forge-darwin-arm64"
|
|
19
|
-
},
|
|
20
|
-
linux: {
|
|
21
|
-
x64: "@foundry-rs/forge-linux-amd64",
|
|
22
|
-
arm64: "@foundry-rs/forge-linux-arm64"
|
|
23
|
-
},
|
|
24
|
-
win32: { x64: "@foundry-rs/forge-win32-amd64" }
|
|
25
|
-
};
|
|
26
|
-
const BINARY_NAME = process.platform === "win32" ? "forge.exe" : "forge";
|
|
27
|
-
const PLATFORM_SPECIFIC_PACKAGE_NAME = BINARY_DISTRIBUTION_PACKAGES[process.platform][process.arch];
|
|
28
|
-
const colors = {
|
|
29
|
-
red: "\x1B[31m",
|
|
30
|
-
green: "\x1B[32m",
|
|
31
|
-
yellow: "\x1B[33m",
|
|
32
|
-
blue: "\x1B[34m",
|
|
33
|
-
magenta: "\x1B[35m",
|
|
34
|
-
cyan: "\x1B[36m",
|
|
35
|
-
white: "\x1B[37m",
|
|
36
|
-
reset: "\x1B[0m"
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
//#endregion
|
|
40
|
-
//#region src/install.ts
|
|
41
|
-
const __dirname = NodePath.dirname(fileURLToPath(import.meta.url));
|
|
42
|
-
const fallbackBinaryPath = NodePath.join(__dirname, BINARY_NAME);
|
|
43
|
-
const require = NodeModule.createRequire(import.meta.url);
|
|
44
|
-
const isLocalhostHost = (hostname) => hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
45
|
-
function ensureSecureUrl(urlString, purpose) {
|
|
46
|
-
try {
|
|
47
|
-
const url = new URL(urlString);
|
|
48
|
-
if (url.protocol === "http:") {
|
|
49
|
-
const allowInsecure = process.env.ALLOW_INSECURE_REGISTRY === "true";
|
|
50
|
-
if (!isLocalhostHost(url.hostname) && !allowInsecure) throw new Error(`Refusing to use insecure HTTP for ${purpose}: ${urlString}. Set ALLOW_INSECURE_REGISTRY=true to override (not recommended).`);
|
|
51
|
-
}
|
|
52
|
-
} catch {}
|
|
53
|
-
}
|
|
54
|
-
function makeRequest(url) {
|
|
55
|
-
return new Promise((resolve, reject) => {
|
|
56
|
-
ensureSecureUrl(url, "HTTP request");
|
|
57
|
-
(url.startsWith("https:") ? NodeHttps : NodeHttp).get(url, (response) => {
|
|
58
|
-
if (response?.statusCode && response.statusCode >= 200 && response.statusCode < 300) {
|
|
59
|
-
const chunks = [];
|
|
60
|
-
response.on("data", (chunk) => chunks.push(chunk));
|
|
61
|
-
response.on("end", () => resolve(Buffer.concat(chunks)));
|
|
62
|
-
} else if (response?.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
63
|
-
const redirected = (() => {
|
|
64
|
-
try {
|
|
65
|
-
return new URL(response.headers.location, url).href;
|
|
66
|
-
} catch {
|
|
67
|
-
return response.headers.location;
|
|
68
|
-
}
|
|
69
|
-
})();
|
|
70
|
-
makeRequest(redirected).then(resolve, reject);
|
|
71
|
-
} else reject(/* @__PURE__ */ new Error(`Package registry responded with status code ${response.statusCode} when downloading the package.`));
|
|
72
|
-
}).on("error", (error) => reject(error));
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Scoped package names should be percent-encoded
|
|
77
|
-
* e.g. @scope/pkg -> %40scope%2Fpkg
|
|
78
|
-
*/
|
|
79
|
-
const encodePackageNameForRegistry = (name) => name.startsWith("@") ? encodeURIComponent(name) : name;
|
|
80
|
-
/**
|
|
81
|
-
* Tar archives are organized in 512 byte blocks.
|
|
82
|
-
* Blocks can either be header blocks or data blocks.
|
|
83
|
-
* Header blocks contain file names of the archive in the first 100 bytes, terminated by a null byte.
|
|
84
|
-
* The size of a file is contained in bytes 124-135 of a header block and in octal format.
|
|
85
|
-
* The following blocks will be data blocks containing the file.
|
|
86
|
-
*/
|
|
87
|
-
function extractFileFromTarball(tarballBuffer, filepath) {
|
|
88
|
-
let offset = 0;
|
|
89
|
-
while (offset < tarballBuffer.length) {
|
|
90
|
-
const header = tarballBuffer.subarray(offset, offset + 512);
|
|
91
|
-
offset += 512;
|
|
92
|
-
const fileName = header.toString("utf-8", 0, 100).replace(/\0.*/g, "");
|
|
93
|
-
const fileSize = Number.parseInt(header.toString("utf-8", 124, 136).replace(/\0.*/g, ""), 8);
|
|
94
|
-
if (fileName === filepath) return tarballBuffer.subarray(offset, offset + fileSize);
|
|
95
|
-
offset = offset + fileSize + 511 & -512;
|
|
96
|
-
}
|
|
97
|
-
throw new Error(`File ${filepath} not found in tarball`);
|
|
98
|
-
}
|
|
99
|
-
async function downloadBinaryFromRegistry() {
|
|
100
|
-
const registryUrl = getRegistryUrl().replace(/\/$/, "");
|
|
101
|
-
ensureSecureUrl(registryUrl, "registry URL");
|
|
102
|
-
const encodedName = encodePackageNameForRegistry(PLATFORM_SPECIFIC_PACKAGE_NAME);
|
|
103
|
-
let desiredVersion;
|
|
104
|
-
try {
|
|
105
|
-
const pkgJsonPath = NodePath.join(__dirname, "..", "package.json");
|
|
106
|
-
const pkgJson = JSON.parse(NodeFS.readFileSync(pkgJsonPath, "utf8"));
|
|
107
|
-
desiredVersion = pkgJson?.optionalDependencies?.[PLATFORM_SPECIFIC_PACKAGE_NAME] || pkgJson?.version;
|
|
108
|
-
} catch {}
|
|
109
|
-
const metaUrl = `${registryUrl}/${encodedName}`;
|
|
110
|
-
const metaBuffer = await makeRequest(metaUrl);
|
|
111
|
-
const metadata = JSON.parse(metaBuffer.toString("utf8"));
|
|
112
|
-
const version = desiredVersion || metadata?.["dist-tags"]?.latest;
|
|
113
|
-
const dist = (metadata?.versions?.[version])?.dist;
|
|
114
|
-
if (!dist?.tarball) throw new Error(`Could not find tarball for ${PLATFORM_SPECIFIC_PACKAGE_NAME}@${version} from ${metaUrl}`);
|
|
115
|
-
ensureSecureUrl(dist.tarball, "tarball URL");
|
|
116
|
-
console.info(colors.green, "Downloading binary from:\n", dist.tarball, "\n", colors.reset);
|
|
117
|
-
/**
|
|
118
|
-
* Download the tarball of the right binary distribution package
|
|
119
|
-
* Verify integrity: prefer SRI integrity (sha512/sha256/sha1),
|
|
120
|
-
* fallback to legacy dist.shasum (sha1 hex). Fail if neither unless explicitly allowed.
|
|
121
|
-
*/
|
|
122
|
-
const tarballDownloadBuffer = await makeRequest(dist.tarball);
|
|
123
|
-
(() => {
|
|
124
|
-
let verified = false;
|
|
125
|
-
const sriMatch = (typeof dist.integrity === "string" ? dist.integrity : "").match(/^([a-z0-9]+)-([A-Za-z0-9+/=]+)$/i);
|
|
126
|
-
if (sriMatch) {
|
|
127
|
-
const algo = sriMatch[1].toLowerCase();
|
|
128
|
-
const expected = sriMatch[2];
|
|
129
|
-
if (new Set([
|
|
130
|
-
"sha512",
|
|
131
|
-
"sha256",
|
|
132
|
-
"sha1"
|
|
133
|
-
]).has(algo)) {
|
|
134
|
-
const actual = NodeCrypto.createHash(algo).update(tarballDownloadBuffer).digest("base64");
|
|
135
|
-
if (expected !== actual) throw new Error(`Downloaded tarball failed integrity check (${algo} mismatch)`);
|
|
136
|
-
verified = true;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
if (!verified && typeof dist.shasum === "string" && dist.shasum.length === 40) {
|
|
140
|
-
const expectedSha1Hex = dist.shasum.toLowerCase();
|
|
141
|
-
const actualSha1Hex = NodeCrypto.createHash("sha1").update(tarballDownloadBuffer).digest("hex");
|
|
142
|
-
if (expectedSha1Hex !== actualSha1Hex) throw new Error("Downloaded tarball failed integrity check (sha1 shasum mismatch)");
|
|
143
|
-
verified = true;
|
|
144
|
-
}
|
|
145
|
-
if (!verified) {
|
|
146
|
-
if (!(process.env.ALLOW_NO_INTEGRITY === "true" || process.env.ALLOW_UNVERIFIED_TARBALL === "true")) throw new Error("No integrity metadata found for downloaded tarball. Set ALLOW_NO_INTEGRITY=true to bypass (not recommended).");
|
|
147
|
-
console.warn(colors.yellow, "Warning: proceeding without integrity verification (explicitly allowed).", colors.reset);
|
|
148
|
-
}
|
|
149
|
-
})();
|
|
150
|
-
const tarballBuffer = NodeZlib.gunzipSync(tarballDownloadBuffer);
|
|
151
|
-
NodeFS.writeFileSync(fallbackBinaryPath, extractFileFromTarball(tarballBuffer, `package/bin/${BINARY_NAME}`), { mode: 493 });
|
|
152
|
-
}
|
|
153
|
-
function isPlatformSpecificPackageInstalled() {
|
|
154
|
-
try {
|
|
155
|
-
require.resolve(`${PLATFORM_SPECIFIC_PACKAGE_NAME}/bin/${BINARY_NAME}`);
|
|
156
|
-
return true;
|
|
157
|
-
} catch (_error) {
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
if (!PLATFORM_SPECIFIC_PACKAGE_NAME) throw new Error("Platform not supported!");
|
|
162
|
-
if (!isPlatformSpecificPackageInstalled()) {
|
|
163
|
-
console.log("Platform specific package not found. Will manually download binary.");
|
|
164
|
-
downloadBinaryFromRegistry().catch((error) => {
|
|
165
|
-
console.error(colors.red, "Failed to download binary:", error, colors.reset);
|
|
166
|
-
process.exitCode = 1;
|
|
167
|
-
});
|
|
168
|
-
} else console.log("Platform specific package already installed. Skipping manual download.");
|
|
169
|
-
|
|
170
|
-
//#endregion
|
|
171
|
-
export { };
|