Skip to content

Commit 5253259

Browse files
author
Raul Melo
committed
add install manager script
1 parent 4eef7e4 commit 5253259

1 file changed

Lines changed: 224 additions & 0 deletions

File tree

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
#!/usr/bin/env node
2+
3+
import * as path from "node:path";
4+
import * as fs from "node:fs";
5+
import https from "https";
6+
import tar from "tar";
7+
import { pipeline } from "stream/promises";
8+
9+
// Mapping from Node's `process.arch` to Golang's `$GOARCH`
10+
const ARCH_MAPPING = {
11+
ia32: "386",
12+
x64: "amd64",
13+
arm: "arm",
14+
arm64: "arm64",
15+
};
16+
17+
// Mapping between Node's `process.platform` to Golang's
18+
const PLATFORM_MAPPING = {
19+
darwin: "darwin",
20+
linux: "linux",
21+
win32: "windows",
22+
freebsd: "freebsd",
23+
};
24+
25+
const command = process.argv[2];
26+
27+
if (command === "install") {
28+
await install();
29+
} else if (command === "uninstall") {
30+
// do something
31+
} else {
32+
console.log(
33+
"Invalid command. 'install' and 'uninstall' are the only supported commands",
34+
);
35+
process.exit(1);
36+
}
37+
38+
async function install() {
39+
console.log("Installing binary...");
40+
validateOsAndArch();
41+
const pkgJson = readPackageJson();
42+
const metaData = getMetaData(pkgJson);
43+
createBinPath(metaData.binPath);
44+
45+
try {
46+
await downloadFile(metaData.url, metaData.binTarGz);
47+
await tar.x({
48+
file: metaData.binTarGz,
49+
cwd: metaData.binPath,
50+
});
51+
52+
console.log("Binary installed successfully");
53+
} catch (error) {
54+
console.error(`Error downloading binary: ${error}`);
55+
process.exit(1);
56+
}
57+
}
58+
59+
function readPackageJson() {
60+
const packageJsonPath = path.join(".", "package.json");
61+
62+
if (!fs.existsSync(packageJsonPath)) {
63+
console.error("package.json not found in current directory");
64+
process.exit(1);
65+
}
66+
67+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
68+
const err = validateConfiguration(packageJson);
69+
70+
if (err) {
71+
console.error(err);
72+
process.exit(1);
73+
}
74+
75+
return packageJson;
76+
}
77+
78+
/**
79+
* @typedef {Object} GoBinary
80+
* @property {(string|undefined)} path - The path of the Go binary.
81+
* @property {(string|undefined)} name - The name of the Go binary.
82+
* @property {(string|undefined)} url - The URL of the Go binary.
83+
*/
84+
85+
/**
86+
* @typedef {Object} PackageJson
87+
* @property {(GoBinary|undefined)} goBinary - The goBinary object.
88+
* @property {string} version - The version of the package.
89+
*/
90+
91+
/**
92+
* @typedef {Object} MetaData
93+
* @property {string} binName - The name of the binary.
94+
* @property {string} binPath - The path of the binary.
95+
* @property {string} url - The URL of the binary.
96+
* @property {string} version - The version of the binary.
97+
*/
98+
99+
/**
100+
* Extracts metadata from a package.json object.
101+
*
102+
* @param {PackageJson} packageJson - The package.json object.
103+
*
104+
* @returns {MetaData} An object containing the binary name, path, URL, and version.
105+
*/
106+
function getMetaData(packageJson) {
107+
const binPath = packageJson.goBinary.path;
108+
let binName = packageJson.goBinary.name;
109+
let url = packageJson.goBinary.url;
110+
let version = packageJson.version;
111+
112+
if (version[0] === "v") {
113+
version = version.substring(1); // strip the 'v' if necessary v0.0.1 => 0.0.1
114+
}
115+
116+
// Binary name on Windows has .exe suffix
117+
if (process.platform === "win32") {
118+
binName += ".exe";
119+
}
120+
121+
// Interpolate variables in URL, if necessary
122+
url = url.replace(/{{arch}}/g, ARCH_MAPPING[process.arch]);
123+
url = url.replace(/{{platform}}/g, PLATFORM_MAPPING[process.platform]);
124+
url = url.replace(/{{version}}/g, version);
125+
url = url.replace(/{{bin_name}}/g, binName);
126+
127+
return {
128+
binName,
129+
binPath,
130+
binFullName: path.join(process.cwd(), binPath),
131+
get binTarGz() {
132+
return `${this.binFullName}.tar.gz`;
133+
},
134+
url,
135+
version,
136+
};
137+
}
138+
139+
function validateOsAndArch() {
140+
if (!(process.arch in ARCH_MAPPING)) {
141+
console.error(`Invalid architecture: ${process.arch}`);
142+
process.exit(1);
143+
}
144+
145+
if (!(process.platform in PLATFORM_MAPPING)) {
146+
console.error(`Invalid platform: ${process.platform} `);
147+
process.exit(1);
148+
}
149+
}
150+
151+
/**
152+
* Validates the package.json object.
153+
* @param {PackageJson} packageJson - The package.json object.
154+
* @returns {string} An error message if the package.json object is invalid.
155+
*/
156+
function validateConfiguration(packageJson) {
157+
if (!packageJson.version) {
158+
return "'version' property must be specified";
159+
}
160+
161+
if (!packageJson.goBinary || typeof packageJson.goBinary !== "object") {
162+
return "'goBinary' property must be defined and be an object";
163+
}
164+
165+
if (!packageJson.goBinary.name) {
166+
return "'name' property is necessary";
167+
}
168+
169+
if (!packageJson.goBinary.path) {
170+
return "'path' property is necessary";
171+
}
172+
173+
if (!packageJson.goBinary.url) {
174+
return "'url' property is required";
175+
}
176+
}
177+
178+
/**
179+
* Creates a directory at the specified path.
180+
* @param {string} binPath - The path of the directory to create.
181+
*/
182+
function createBinPath(binPath) {
183+
if (!fs.existsSync(binPath)) {
184+
fs.mkdirSync(binPath, { recursive: true });
185+
}
186+
}
187+
188+
/**
189+
* Downloads a file from a given URL and saves it to the specified path.
190+
*
191+
* @param {string} url - The URL of the file to download.
192+
* @param {string} outputPath - The path where the downloaded file should be saved.
193+
* @returns {Promise<string>} A promise that resolves with the path of the downloaded file.
194+
* @throws {Error} Throws an error if the download or file writing fails.
195+
*/
196+
async function downloadFile(url, outputPath) {
197+
return new Promise((resolve, reject) => {
198+
const processResponse = (response) => {
199+
// Check if the response is a redirect
200+
if (
201+
response.statusCode >= 300 &&
202+
response.statusCode < 400 &&
203+
response.headers.location
204+
) {
205+
https
206+
.get(response.headers.location, processResponse)
207+
.on("error", reject);
208+
} else if (response.statusCode === 200) {
209+
const fileStream = fs.createWriteStream(outputPath);
210+
pipeline(response, fileStream)
211+
.then(() => resolve(outputPath))
212+
.catch((error) => {
213+
reject(`Error during download: ${error.message}`);
214+
});
215+
} else {
216+
reject(
217+
`Server responded with ${response.statusCode}: ${response.statusMessage}`,
218+
);
219+
}
220+
};
221+
222+
https.get(url, processResponse).on("error", reject);
223+
});
224+
}

0 commit comments

Comments
 (0)