mirror of
https://github.com/rancher-sandbox/rancher-desktop.git
synced 2021-10-13 00:04:06 +03:00
scripts: Download Linux binaries on Windows.
This also includes refactoring so the download bits don't just grab functions out of a random script. Signed-off-by: Mark Yen <mark.yen@suse.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
/node_modules/
|
||||
.DS_Store
|
||||
/resources/darwin/
|
||||
/resources/linux/
|
||||
/resources/win32/
|
||||
/coverage/
|
||||
/dist/**
|
||||
|
||||
@@ -1,334 +0,0 @@
|
||||
import childProcess from 'child_process';
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
/**
|
||||
* Execute a process and wait for it to finish.
|
||||
* @param command {readonly string} The executable to run.
|
||||
* @param args {readonly string[]} Arguments to the executable.
|
||||
*/
|
||||
function spawnSync(command, ...args) {
|
||||
/** @type {childProcess.SpawnOptions} */
|
||||
const options = { stdio: 'inherit', windowsHide: true };
|
||||
const { status, signal, error } = childProcess.spawnSync(command, args, options);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
if (signal !== null && signal !== 'SIGTERM') {
|
||||
throw new Error(`${ command } exited with signal ${ signal }`);
|
||||
}
|
||||
if (status !== null && status !== 0) {
|
||||
throw new Error(`${ command } exited with status ${ status }`);
|
||||
}
|
||||
}
|
||||
|
||||
/** The platform string, as used by golang / Kubernetes. */
|
||||
const kubePlatform = {
|
||||
darwin: 'darwin',
|
||||
linux: 'linux',
|
||||
win32: 'windows',
|
||||
}[os.platform()];
|
||||
const resourcesDir = path.join(process.cwd(), 'resources', os.platform());
|
||||
const binDir = path.join(resourcesDir, 'bin');
|
||||
const onWindows = kubePlatform === 'windows';
|
||||
|
||||
function exeName(name) {
|
||||
return `${ name }${ onWindows ? '.exe' : '' }`;
|
||||
}
|
||||
|
||||
async function getChecksumForFile(inputPath, checksumAlgorithm = 'sha256') {
|
||||
const hash = crypto.createHash(checksumAlgorithm);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
hash.on('finish', resolve);
|
||||
fs.createReadStream(inputPath).pipe(hash);
|
||||
});
|
||||
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef DownloadOptions Object
|
||||
* @prop {string} [expectedChecksum] The expected checksum for the file.
|
||||
* @prop {string} [checksumAlgorithm="sha256"] Checksum algorithm.
|
||||
* @prop {boolean} [overwrite=false] Whether to re-download files that already exist.
|
||||
* @prop {number} [access=fs.constants.X_OK] The file mode required.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Download the given URL, making the result executable
|
||||
* @param url {string} The URL to download
|
||||
* @param destPath {string} The path to download to
|
||||
* @param options {DownloadOptions} Additional options for the download.
|
||||
*/
|
||||
export async function download(url, destPath, options = {}) {
|
||||
const { expectedChecksum, overwrite } = options;
|
||||
const checksumAlgorithm = options.checksumAlgorithm ?? 'sha256';
|
||||
const access = options.access ?? fs.constants.X_OK;
|
||||
|
||||
if (!overwrite) {
|
||||
try {
|
||||
await fs.promises.access(destPath, access);
|
||||
console.log(`${ destPath } already exists, not re-downloading.`);
|
||||
|
||||
return;
|
||||
} catch (ex) {
|
||||
if (ex.code !== 'ENOENT') {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`Downloading ${ url } to ${ destPath }...`);
|
||||
await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error downloading ${ url }: ${ response.statusText }`);
|
||||
}
|
||||
const tempPath = `${ destPath }.download`;
|
||||
|
||||
try {
|
||||
const file = fs.createWriteStream(tempPath);
|
||||
const promise = new Promise(resolve => file.on('finish', resolve));
|
||||
|
||||
response.body.pipe(file);
|
||||
await promise;
|
||||
|
||||
if (expectedChecksum) {
|
||||
const actualChecksum = await getChecksumForFile(tempPath, checksumAlgorithm);
|
||||
|
||||
if (actualChecksum !== expectedChecksum) {
|
||||
throw new Error(`Expecting URL ${ url } to have ${ checksumAlgorithm } [${ expectedChecksum }], got [${ actualChecksum }]`);
|
||||
}
|
||||
}
|
||||
const mode =
|
||||
(access & fs.constants.X_OK) ? 0o755 : (access & fs.constants.W_OK) ? 0o644 : 0o444;
|
||||
|
||||
await fs.promises.chmod(tempPath, mode);
|
||||
await fs.promises.rename(tempPath, destPath);
|
||||
} finally {
|
||||
try {
|
||||
await fs.promises.unlink(tempPath);
|
||||
} catch (ex) {
|
||||
if (ex.code !== 'ENOENT') {
|
||||
console.error(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a tar.gz file to a temp dir, expand,
|
||||
* and move the expected binary to the final dir
|
||||
*
|
||||
* @param url {string} The URL to download.
|
||||
* @param expectedChecksum {string} The URL's hash URL; empty string turns off sha checking.
|
||||
* @param binaryBasename {string} The base name of the executable to find.
|
||||
* @param platformDir {string} The platform-specific part of the path that holds the expanded executable.
|
||||
* @returns {Promise<string>} The full path of the final binary if successful, '' otherwise.
|
||||
*/
|
||||
async function downloadTarGZ(url, expectedChecksum, binaryBasename, platformDir) {
|
||||
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), `rd-${ binaryBasename }-`));
|
||||
let binaryFinalPath = '';
|
||||
const fileToExtract = path.join(platformDir, exeName(binaryBasename));
|
||||
|
||||
try {
|
||||
const tgzPath = path.join(workDir, `${ binaryBasename }.tar.gz`);
|
||||
const args = ['tar', '-zxvf', tgzPath, '--directory', workDir, fileToExtract.replace(/\\/g, '/')];
|
||||
|
||||
await download(url, tgzPath, { expectedChecksum, access: fs.constants.W_OK });
|
||||
if (onWindows) {
|
||||
// On Windows, force use the bundled bsdtar.
|
||||
// We may find GNU tar on the path, which looks at the Windows-style path
|
||||
// and considers C:\Temp to be a reference to a remote host named `C`.
|
||||
args[0] = path.join(process.env.SystemRoot, 'system32', 'tar.exe');
|
||||
}
|
||||
spawnSync(...args);
|
||||
binaryFinalPath = path.join(binDir, exeName(binaryBasename));
|
||||
fs.copyFileSync(path.join(workDir, fileToExtract), binaryFinalPath);
|
||||
fs.chmodSync(binaryFinalPath, 0o755);
|
||||
} finally {
|
||||
console.log('finishing...');
|
||||
fs.rmSync(workDir, { recursive: true, maxRetries: 10 });
|
||||
}
|
||||
|
||||
return binaryFinalPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a zip to a temp dir, expand,
|
||||
* and move the expected binary to the final dir
|
||||
*
|
||||
* @param url {string} The URL to download.
|
||||
* @param expectedChecksum {string} The URL's hash URL; empty string turns off sha checking.
|
||||
* @param binaryBasename {string} The base name of the executable to find.
|
||||
* @param platformDir {string} The platform-specific part of the path that holds the expanded executable.
|
||||
* @returns {Promise<string>} The full path of the final binary if successful, '' otherwise.
|
||||
*/
|
||||
async function downloadZip(url, expectedChecksum, binaryBasename, platformDir) {
|
||||
const zipDir = fs.mkdtempSync(path.join(os.tmpdir(), `rd-${ binaryBasename }-`));
|
||||
let binaryFinalPath = '';
|
||||
const fileToExtract = path.join(platformDir, exeName(binaryBasename));
|
||||
|
||||
try {
|
||||
const zipPath = path.join(zipDir, `${ binaryBasename }.zip`);
|
||||
const args = ['unzip', '-o', zipPath, fileToExtract.replace(/\\/g, '/'), '-d', zipDir];
|
||||
|
||||
await download(url, zipPath, { expectedChecksum, access: fs.constants.W_OK });
|
||||
spawnSync(...args);
|
||||
binaryFinalPath = path.join(binDir, exeName(binaryBasename));
|
||||
fs.copyFileSync(path.join(zipDir, fileToExtract), binaryFinalPath);
|
||||
fs.chmodSync(binaryFinalPath, 0o755);
|
||||
} finally {
|
||||
console.log('finishing...');
|
||||
fs.rmSync(zipDir, { recursive: true, maxRetries: 10 });
|
||||
}
|
||||
|
||||
return binaryFinalPath;
|
||||
}
|
||||
|
||||
export async function getResource(url) {
|
||||
return await (await fetch(url)).text();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the home directory, in a way that is compatible with
|
||||
* kuberlr
|
||||
*/
|
||||
async function findHome() {
|
||||
const tryAccess = async(path) => {
|
||||
try {
|
||||
await fs.promises.access(path);
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const osHomeDir = os.homedir();
|
||||
|
||||
if (osHomeDir && await tryAccess(osHomeDir)) {
|
||||
return osHomeDir;
|
||||
}
|
||||
if (process.env.HOME && await tryAccess(process.env.HOME)) {
|
||||
return process.env.HOME;
|
||||
}
|
||||
if (onWindows) {
|
||||
if (process.env.USERPROFILE && await tryAccess(process.env.USERPROFILE)) {
|
||||
return process.env.USERPROFILE;
|
||||
}
|
||||
if (process.env.HOMEDRIVE && process.env.HOMEPATH) {
|
||||
const homePath = path.join(process.env.HOMEDRIVE, process.env.HOMEPATH);
|
||||
|
||||
if (await tryAccess(homePath)) {
|
||||
return homePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function downloadKuberlr(kuberlrBaseURL, finalKuberlrSHA, kuberlrPlatformDir, onWindows) {
|
||||
if (onWindows) {
|
||||
return await downloadZip(`${ kuberlrBaseURL }.zip`, finalKuberlrSHA, 'kuberlr', kuberlrPlatformDir);
|
||||
}
|
||||
|
||||
return await downloadTarGZ(`${ kuberlrBaseURL }.tar.gz`, finalKuberlrSHA, 'kuberlr', kuberlrPlatformDir);
|
||||
}
|
||||
|
||||
export default async function main() {
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
const kuberlrVersion = '0.3.2';
|
||||
const kuberlrBase = `https://github.com/flavio/kuberlr/releases/download/v${ kuberlrVersion }`;
|
||||
const kuberlrBaseURL = `${ kuberlrBase }/kuberlr_${ kuberlrVersion }_${ kubePlatform }_amd64`;
|
||||
const kuberlrPlatformDir = `kuberlr_${ kuberlrVersion }_${ kubePlatform }_amd64`;
|
||||
const allKuberlrSHAs = await getResource(`${ kuberlrBase }/checksums.txt`);
|
||||
const kuberlrSHA = allKuberlrSHAs.split(/\r?\n/).filter(line => line.includes(`kuberlr_${ kuberlrVersion }_${ kubePlatform }_amd64`));
|
||||
|
||||
switch (kuberlrSHA.length) {
|
||||
case 0:
|
||||
throw new Error(`Couldn't find a matching SHA for [kuberlr_${ kuberlrVersion }_${ kubePlatform }-amd64] in [${ allKuberlrSHAs }]`);
|
||||
case 1:
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Matched ${ kuberlrSHA.length } hits, not exactly 1, for platform ${ kubePlatform } in [${ allKuberlrSHAs }]`);
|
||||
}
|
||||
const finalKuberlrSHA = kuberlrSHA[0].split(/\s+/, 1)[0];
|
||||
const kuberlrPath = await downloadKuberlr(kuberlrBaseURL, finalKuberlrSHA, kuberlrPlatformDir, onWindows);
|
||||
|
||||
// Download Kubectl into kuberlr's directory of versioned kubectl's
|
||||
const kubeVersion = (await getResource('https://dl.k8s.io/release/stable.txt')).trim();
|
||||
const kubectlURL = `https://dl.k8s.io/${ kubeVersion }/bin/${ kubePlatform }/amd64/${ exeName('kubectl') }`;
|
||||
const kubectlSHA = await getResource(`${ kubectlURL }.sha256`);
|
||||
const kuberlrDir = path.join(await findHome(), '.kuberlr', `${ kubePlatform }-amd64`);
|
||||
const managedKubectlPath = path.join(kuberlrDir, exeName(`kubectl${ kubeVersion.replace(/^v/, '') }`));
|
||||
|
||||
await download(kubectlURL, managedKubectlPath, { expectedChecksum: kubectlSHA });
|
||||
await bindKubectlToKuberlr(kuberlrPath);
|
||||
|
||||
// Download Helm. It is a tar.gz file that needs to be expanded and file moved.
|
||||
const helmVersion = '3.6.1';
|
||||
const helmURL = `https://get.helm.sh/helm-v${ helmVersion }-${ kubePlatform }-amd64.tar.gz`;
|
||||
const helmSHA = (await getResource(`${ helmURL }.sha256sum`)).split(/\s+/, 1)[0];
|
||||
|
||||
await downloadTarGZ(helmURL, helmSHA, 'helm', `${ kubePlatform }-amd64`);
|
||||
|
||||
// Download Kim
|
||||
const kimVersion = '0.1.0-beta.2';
|
||||
const kimURLBase = `https://github.com/rancher/kim/releases/download/v${ kimVersion }`;
|
||||
const kimURL = `${ kimURLBase }/${ exeName(`kim-${ kubePlatform }-amd64`) }`;
|
||||
const kimPath = path.join(binDir, exeName( 'kim'));
|
||||
const allKimSHAs = await getResource(`${ kimURLBase }/sha256sum.txt`);
|
||||
const kimSHA = allKimSHAs.split(/\r?\n/).filter(line => line.includes(`kim-${ kubePlatform }-amd64`));
|
||||
|
||||
switch (kimSHA.length) {
|
||||
case 0:
|
||||
throw new Error(`Couldn't find a matching SHA for [kim-${ kubePlatform }-amd64] in [${ allKimSHAs }]`);
|
||||
case 1:
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Matched ${ kimSHA.length } hits, not exactly 1, for platform ${ kubePlatform } in [${ allKimSHAs }]`);
|
||||
}
|
||||
await download(kimURL, kimPath, { expectedChecksum: kimSHA[0].split(/\s+/, 1)[0] });
|
||||
}
|
||||
|
||||
/**
|
||||
* Desired: on Windows, .../bin/kubectl.exe is a copy of .../bin/kuberlr.exe
|
||||
* elsewhere: .../bin/kubectl is a symlink to .../bin/kuberlr
|
||||
* @param kuberlrPath {string}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function bindKubectlToKuberlr(kuberlrPath) {
|
||||
const binKubectlPath = path.join(binDir, exeName('kubectl'));
|
||||
|
||||
if (onWindows) {
|
||||
await fs.promises.copyFile(kuberlrPath, binKubectlPath);
|
||||
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const binKubectlStat = await fs.promises.lstat(binKubectlPath);
|
||||
|
||||
if (binKubectlStat.isSymbolicLink()) {
|
||||
const actualTarget = await fs.promises.readlink(binKubectlPath);
|
||||
|
||||
if (actualTarget === 'kuberlr') {
|
||||
// The link is already there
|
||||
return;
|
||||
} else {
|
||||
console.log(`Deleting symlink ${ binKubectlPath } unexpectedly pointing to ${ actualTarget }`);
|
||||
}
|
||||
}
|
||||
await fs.promises.rm(binKubectlPath);
|
||||
} catch (_) {
|
||||
// .../bin/kubectl doesn't exist, so there's nothing to clean up
|
||||
}
|
||||
await fs.promises.symlink('kuberlr', binKubectlPath);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import childProcess from 'child_process';
|
||||
import path from 'path';
|
||||
import util from 'util';
|
||||
|
||||
import { download } from '../download-resources.mjs';
|
||||
import { download } from '../lib/download.mjs';
|
||||
|
||||
// The version of hyperkit to build
|
||||
const ver = 'v0.20210107';
|
||||
|
||||
@@ -4,7 +4,7 @@ import childProcess from 'child_process';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { download, getResource } from '../download-resources.mjs';
|
||||
import { download, getResource } from '../lib/download.mjs';
|
||||
|
||||
const limaRepo = 'https://github.com/rancher-sandbox/lima';
|
||||
const limaTag = 'v0.5.0';
|
||||
|
||||
170
scripts/download/tools.mjs
Normal file
170
scripts/download/tools.mjs
Normal file
@@ -0,0 +1,170 @@
|
||||
import childProcess from 'child_process';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { download, downloadZip, downloadTarGZ, getResource } from '../lib/download.mjs';
|
||||
|
||||
/**
|
||||
* Find the home directory, in a way that is compatible with kuberlr
|
||||
*
|
||||
* @param {boolean} [onWindows] Whether we're running on Windows
|
||||
*/
|
||||
async function findHome(onWindows) {
|
||||
const tryAccess = async(path) => {
|
||||
try {
|
||||
await fs.promises.access(path);
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const osHomeDir = os.homedir();
|
||||
|
||||
if (osHomeDir && await tryAccess(osHomeDir)) {
|
||||
return osHomeDir;
|
||||
}
|
||||
if (process.env.HOME && await tryAccess(process.env.HOME)) {
|
||||
return process.env.HOME;
|
||||
}
|
||||
if (onWindows) {
|
||||
if (process.env.USERPROFILE && await tryAccess(process.env.USERPROFILE)) {
|
||||
return process.env.USERPROFILE;
|
||||
}
|
||||
if (process.env.HOMEDRIVE && process.env.HOMEPATH) {
|
||||
const homePath = path.join(process.env.HOMEDRIVE, process.env.HOMEPATH);
|
||||
|
||||
if (await tryAccess(homePath)) {
|
||||
return homePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function downloadKuberlr(kubePlatform, destDir) {
|
||||
const kuberlrVersion = '0.3.2';
|
||||
const baseURL = `https://github.com/flavio/kuberlr/releases/download/v${ kuberlrVersion }`;
|
||||
const platformDir = `kuberlr_${ kuberlrVersion }_${ kubePlatform }_amd64`;
|
||||
const archiveName = platformDir + (kubePlatform.startsWith('win') ? '.zip' : '.tar.gz');
|
||||
const exeName = kubePlatform.startsWith('win') ? 'kuberlr.exe' : 'kuberlr';
|
||||
|
||||
const allChecksums = (await getResource(`${ baseURL }/checksums.txt`)).split(/\r?\n/);
|
||||
const checksums = allChecksums.filter(line => line.includes(platformDir));
|
||||
|
||||
switch (checksums.length) {
|
||||
case 0:
|
||||
throw new Error(`Couldn't find a matching SHA for [${ platformDir }] in [${ allChecksums }]`);
|
||||
case 1:
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Matched ${ checksums.length } hits, not exactly 1, for platform ${ kubePlatform } in [${ allChecksums }]`);
|
||||
}
|
||||
|
||||
/** @type import('../lib/download.mjs').ArchiveDownloadOptions */
|
||||
const options = {
|
||||
expectedChecksum: checksums[0].split(/\s+/)[0],
|
||||
entryName: `${ platformDir }/${ exeName }`,
|
||||
};
|
||||
|
||||
if (kubePlatform.startsWith('win')) {
|
||||
return await downloadZip(`${ baseURL }/${ archiveName }`, path.join(destDir, exeName), options);
|
||||
}
|
||||
|
||||
return await downloadTarGZ(`${ baseURL }/${ archiveName }`, path.join(destDir, exeName), options);
|
||||
}
|
||||
|
||||
export default async function main(platform) {
|
||||
/** The platform string, as used by golang / Kubernetes. */
|
||||
const kubePlatform = {
|
||||
darwin: 'darwin',
|
||||
linux: 'linux',
|
||||
win32: 'windows',
|
||||
}[platform];
|
||||
const resourcesDir = path.join(process.cwd(), 'resources', platform);
|
||||
const binDir = path.join(resourcesDir, 'bin');
|
||||
const onWindows = kubePlatform === 'windows';
|
||||
|
||||
function exeName(name) {
|
||||
return `${ name }${ onWindows ? '.exe' : '' }`;
|
||||
}
|
||||
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
const kuberlrPath = await downloadKuberlr(kubePlatform, binDir);
|
||||
|
||||
await bindKubectlToKuberlr(kuberlrPath, path.join(binDir, exeName('kubectl')));
|
||||
|
||||
// Download Kubectl into kuberlr's directory of versioned kubectl's
|
||||
if (platform === os.platform()) {
|
||||
const kubeVersion = (await getResource('https://dl.k8s.io/release/stable.txt')).trim();
|
||||
const kubectlURL = `https://dl.k8s.io/${ kubeVersion }/bin/${ kubePlatform }/amd64/${ exeName('kubectl') }`;
|
||||
const kubectlSHA = await getResource(`${ kubectlURL }.sha256`);
|
||||
const kuberlrDir = path.join(await findHome(onWindows), '.kuberlr', `${ kubePlatform }-amd64`);
|
||||
const managedKubectlPath = path.join(kuberlrDir, exeName(`kubectl${ kubeVersion.replace(/^v/, '') }`));
|
||||
|
||||
await download(kubectlURL, managedKubectlPath, { expectedChecksum: kubectlSHA });
|
||||
}
|
||||
|
||||
// Download Helm. It is a tar.gz file that needs to be expanded and file moved.
|
||||
const helmVersion = '3.6.1';
|
||||
const helmURL = `https://get.helm.sh/helm-v${ helmVersion }-${ kubePlatform }-amd64.tar.gz`;
|
||||
|
||||
await downloadTarGZ(helmURL, path.join(binDir, exeName('helm')), {
|
||||
expectedChecksum: (await getResource(`${ helmURL }.sha256sum`)).split(/\s+/, 1)[0],
|
||||
entryName: `${ kubePlatform }-amd64/${ exeName('helm') }`,
|
||||
});
|
||||
|
||||
// Download Kim
|
||||
const kimVersion = '0.1.0-beta.2';
|
||||
const kimURLBase = `https://github.com/rancher/kim/releases/download/v${ kimVersion }`;
|
||||
const kimURL = `${ kimURLBase }/${ exeName(`kim-${ kubePlatform }-amd64`) }`;
|
||||
const kimPath = path.join(binDir, exeName('kim'));
|
||||
const allKimSHAs = await getResource(`${ kimURLBase }/sha256sum.txt`);
|
||||
const kimSHA = allKimSHAs.split(/\r?\n/).filter(line => line.includes(`kim-${ kubePlatform }-amd64`));
|
||||
|
||||
switch (kimSHA.length) {
|
||||
case 0:
|
||||
throw new Error(`Couldn't find a matching SHA for [kim-${ kubePlatform }-amd64] in [${ allKimSHAs }]`);
|
||||
case 1:
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Matched ${ kimSHA.length } hits, not exactly 1, for platform ${ kubePlatform } in [${ allKimSHAs }]`);
|
||||
}
|
||||
await download(kimURL, kimPath, { expectedChecksum: kimSHA[0].split(/\s+/, 1)[0] });
|
||||
}
|
||||
|
||||
/**
|
||||
* Desired: on Windows, .../bin/kubectl.exe is a copy of .../bin/kuberlr.exe
|
||||
* elsewhere: .../bin/kubectl is a symlink to .../bin/kuberlr
|
||||
* @param kuberlrPath {string}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function bindKubectlToKuberlr(kuberlrPath, binKubectlPath) {
|
||||
if (os.platform().startsWith('win')) {
|
||||
await fs.promises.copyFile(kuberlrPath, binKubectlPath);
|
||||
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const binKubectlStat = await fs.promises.lstat(binKubectlPath);
|
||||
|
||||
if (binKubectlStat.isSymbolicLink()) {
|
||||
const actualTarget = await fs.promises.readlink(binKubectlPath);
|
||||
|
||||
if (actualTarget === 'kuberlr') {
|
||||
// The link is already there
|
||||
return;
|
||||
} else {
|
||||
console.log(`Deleting symlink ${ binKubectlPath } unexpectedly pointing to ${ actualTarget }`);
|
||||
}
|
||||
}
|
||||
await fs.promises.rm(binKubectlPath);
|
||||
} catch (_) {
|
||||
// .../bin/kubectl doesn't exist, so there's nothing to clean up
|
||||
}
|
||||
await fs.promises.symlink('kuberlr', binKubectlPath);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { download } from '../download-resources.mjs';
|
||||
import { download } from '../lib/download.mjs';
|
||||
|
||||
export default async function main() {
|
||||
await download(
|
||||
|
||||
211
scripts/lib/download.mjs
Normal file
211
scripts/lib/download.mjs
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Helpers for downloading files.
|
||||
*/
|
||||
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { spawnSync } from 'child_process';
|
||||
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
/**
|
||||
* @typedef DownloadOptions Object
|
||||
* @prop {string} [expectedChecksum] The expected checksum for the file.
|
||||
* @prop {string} [checksumAlgorithm="sha256"] Checksum algorithm.
|
||||
* @prop {boolean} [overwrite=false] Whether to re-download files that already exist.
|
||||
* @prop {number} [access=fs.constants.X_OK] The file mode required.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Download the given URL, making the result executable
|
||||
* @param {string} [url] The URL to download
|
||||
* @param {string} [destPath] The path to download to
|
||||
* @param {DownloadOptions} [options] Additional options for the download.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function download(url, destPath, options = {}) {
|
||||
const { expectedChecksum, overwrite } = options;
|
||||
const checksumAlgorithm = options.checksumAlgorithm ?? 'sha256';
|
||||
const access = options.access ?? fs.constants.X_OK;
|
||||
|
||||
if (!overwrite) {
|
||||
try {
|
||||
await fs.promises.access(destPath, access);
|
||||
console.log(`${ destPath } already exists, not re-downloading.`);
|
||||
|
||||
return;
|
||||
} catch (ex) {
|
||||
if (ex.code !== 'ENOENT') {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`Downloading ${ url } to ${ destPath }...`);
|
||||
await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error downloading ${ url }: ${ response.statusText }`);
|
||||
}
|
||||
const tempPath = `${ destPath }.download`;
|
||||
|
||||
try {
|
||||
const file = fs.createWriteStream(tempPath);
|
||||
const promise = new Promise(resolve => file.on('finish', resolve));
|
||||
|
||||
response.body.pipe(file);
|
||||
await promise;
|
||||
|
||||
if (expectedChecksum) {
|
||||
const actualChecksum = await getChecksumForFile(tempPath, checksumAlgorithm);
|
||||
|
||||
if (actualChecksum !== expectedChecksum) {
|
||||
throw new Error(`Expecting URL ${ url } to have ${ checksumAlgorithm } [${ expectedChecksum }], got [${ actualChecksum }]`);
|
||||
}
|
||||
}
|
||||
const mode =
|
||||
(access & fs.constants.X_OK) ? 0o755 : (access & fs.constants.W_OK) ? 0o644 : 0o444;
|
||||
|
||||
await fs.promises.chmod(tempPath, mode);
|
||||
await fs.promises.rename(tempPath, destPath);
|
||||
} finally {
|
||||
try {
|
||||
await fs.promises.unlink(tempPath);
|
||||
} catch (ex) {
|
||||
if (ex.code !== 'ENOENT') {
|
||||
console.error(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the checksum for a given file
|
||||
* @param {string} inputPath The file to checksum.
|
||||
* @param {'sha256' | 'sha1'} checksumAlgorithm The checksum algorithm to use.
|
||||
* @returns {string} The hex-encoded checksum of the file.
|
||||
*/
|
||||
async function getChecksumForFile(inputPath, checksumAlgorithm = 'sha256') {
|
||||
const hash = crypto.createHash(checksumAlgorithm);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
hash.on('finish', resolve);
|
||||
fs.createReadStream(inputPath).pipe(hash);
|
||||
});
|
||||
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the contents of a given URL.
|
||||
* @param {string} url The URL to download
|
||||
* @returns {string} The file contents.
|
||||
*/
|
||||
export async function getResource(url) {
|
||||
return await (await fetch(url)).text();
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef ArchiveDownloadOptions DownloadOptions
|
||||
* @prop {string} [entryName] The name in the archive of the file; defaults to base name of the destination.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Download a tar.gz file to a temp dir, expand,
|
||||
* and move the expected binary to the final dir
|
||||
*
|
||||
* @param url {string} The URL to download.
|
||||
* @param destPath {string} The path to download to, including the executable name.
|
||||
* @param options {ArchiveDownloadOptions} Additional options for the download.
|
||||
* @returns {Promise<string>} The full path of the final binary.
|
||||
*/
|
||||
export async function downloadTarGZ(url, destPath, options = {}) {
|
||||
const { overwrite } = options;
|
||||
const access = options.access ?? fs.constants.X_OK;
|
||||
|
||||
if (!overwrite) {
|
||||
try {
|
||||
await fs.promises.access(destPath, access);
|
||||
console.log(`${ destPath } already exists, not re-downloading.`);
|
||||
|
||||
return destPath;
|
||||
} catch (ex) {
|
||||
if (ex.code !== 'ENOENT') {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
const binaryBasename = path.basename(destPath, '.exe');
|
||||
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), `rd-${ binaryBasename }-`));
|
||||
const fileToExtract = options.entryName || path.basename(destPath);
|
||||
|
||||
try {
|
||||
const tgzPath = path.join(workDir, `${ binaryBasename }.tar.gz`);
|
||||
const args = ['tar', '-zxvf', tgzPath, '--directory', workDir, fileToExtract];
|
||||
const mode =
|
||||
(access & fs.constants.X_OK) ? 0o755 : (access & fs.constants.W_OK) ? 0o644 : 0o444;
|
||||
|
||||
await download(url, tgzPath, { ...options, access: fs.constants.W_OK });
|
||||
if (os.platform().startsWith('win')) {
|
||||
// On Windows, force use the bundled bsdtar.
|
||||
// We may find GNU tar on the path, which looks at the Windows-style path
|
||||
// and considers C:\Temp to be a reference to a remote host named `C`.
|
||||
args[0] = path.join(process.env.SystemRoot, 'system32', 'tar.exe');
|
||||
}
|
||||
spawnSync(args[0], args.slice(1), { stdio: 'inherit' });
|
||||
fs.copyFileSync(path.join(workDir, fileToExtract), destPath);
|
||||
fs.chmodSync(destPath, mode);
|
||||
} finally {
|
||||
fs.rmSync(workDir, { recursive: true, maxRetries: 10 });
|
||||
}
|
||||
|
||||
return destPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a zip file to a temp dir, expand,
|
||||
* and move the expected binary to the final dir
|
||||
*
|
||||
* @param url {string} The URL to download.
|
||||
* @param destPath {string} The path to download to, including the executable name.
|
||||
* @param options {ArchiveDownloadOptions} Additional options for the download.
|
||||
* @returns {Promise<string>} The full path of the final binary.
|
||||
*/
|
||||
export async function downloadZip(url, destPath, options = {}) {
|
||||
const { overwrite } = options;
|
||||
const access = options.access ?? fs.constants.X_OK;
|
||||
|
||||
if (!overwrite) {
|
||||
try {
|
||||
await fs.promises.access(destPath, access);
|
||||
console.log(`${ destPath } already exists, not re-downloading.`);
|
||||
|
||||
return destPath;
|
||||
} catch (ex) {
|
||||
if (ex.code !== 'ENOENT') {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
const binaryBasename = path.basename(destPath, '.exe');
|
||||
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), `rd-${ binaryBasename }-`));
|
||||
const fileToExtract = options.entryName || path.basename(destPath);
|
||||
const mode =
|
||||
(access & fs.constants.X_OK) ? 0o755 : (access & fs.constants.W_OK) ? 0o644 : 0o444;
|
||||
|
||||
try {
|
||||
const zipPath = path.join(workDir, `${ binaryBasename }.tar.gz`);
|
||||
const args = ['unzip', '-o', zipPath, fileToExtract, '-d', workDir];
|
||||
|
||||
await download(url, zipPath, { ...options, access: fs.constants.W_OK });
|
||||
spawnSync(args[0], args.slice(1), { stdio: 'inherit' });
|
||||
fs.copyFileSync(path.join(workDir, fileToExtract), destPath);
|
||||
fs.chmodSync(destPath, mode);
|
||||
} finally {
|
||||
fs.rmSync(workDir, { recursive: true, maxRetries: 10 });
|
||||
}
|
||||
|
||||
return destPath;
|
||||
}
|
||||
@@ -2,19 +2,18 @@ import { execFileSync } from 'child_process';
|
||||
import os from 'os';
|
||||
|
||||
async function runScripts() {
|
||||
const scripts = ['download-resources'];
|
||||
|
||||
switch (os.platform()) {
|
||||
case 'darwin':
|
||||
scripts.push('download/hyperkit', 'download/lima');
|
||||
await (await import('./download/tools.mjs')).default('darwin');
|
||||
await (await import('./download/hyperkit.mjs')).default();
|
||||
await (await import('./download/lima.mjs')).default();
|
||||
break;
|
||||
case 'win32':
|
||||
scripts.push('download/wsl');
|
||||
await (await import('./download/tools.mjs')).default('win32');
|
||||
await (await import('./download/tools.mjs')).default('linux');
|
||||
await (await import('./download/wsl.mjs')).default();
|
||||
break;
|
||||
}
|
||||
for (const script of scripts) {
|
||||
await (await import(`./${ script }.mjs`)).default();
|
||||
}
|
||||
}
|
||||
|
||||
runScripts().then(() => {
|
||||
|
||||
Reference in New Issue
Block a user