diff --git a/.gitignore b/.gitignore index 5ec0852e..a16b4ff3 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ balenarc.yml build/ build-bin/ build-zip/ +dist/ # Ignore fast-boot cache file **/.fast-boot.json diff --git a/.travis.yml b/.travis.yml index bab23522..dc306149 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,15 +3,21 @@ os: - linux - osx node_js: - - "8" -before_install: -- npm -g install npm@4 -script: npm run ci + - "10" +script: + - node --version + - npm --version + - npm run ci notifications: email: false deploy: - provider: script - script: npm run release + script: + - node --version + - npm --version + - npm run build:standalone + - npm run build:installer + - npm run release skip_cleanup: true on: tags: true diff --git a/appveyor.yml b/appveyor.yml index 92f6bf2f..8eb60841 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,8 @@ # appveyor file # http://www.appveyor.com/docs/appveyor-yml +image: Visual Studio 2017 + init: - git config --global core.autocrlf input @@ -14,12 +16,12 @@ matrix: # what combinations to test environment: matrix: - - nodejs_version: 8 + - nodejs_version: 10 install: - ps: Install-Product node $env:nodejs_version x64 - - npm install -g npm@4 - set PATH=%APPDATA%\npm;%PATH% + - npm config set python 'C:\Python27\python.exe' - npm install build: off @@ -27,8 +29,12 @@ build: off test_script: - node --version - npm --version - - cmd: npm test + - npm test deploy_script: + - node --version + - npm --version + - npm run build:standalone + - npm run build:installer - IF "%APPVEYOR_REPO_TAG%" == "true" (npm run release) - IF NOT "%APPVEYOR_REPO_TAG%" == "true" (echo 'Not tagged, skipping deploy') diff --git a/automation/build-bin.ts b/automation/build-bin.ts old mode 100755 new mode 100644 index 506dd536..43348c69 --- a/automation/build-bin.ts +++ b/automation/build-bin.ts @@ -14,50 +14,106 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { run as oclifRun } from '@oclif/dev-cli'; import * as Bluebird from 'bluebird'; import * as filehound from 'filehound'; import * as fs from 'fs-extra'; import * as path from 'path'; import { exec as execPkg } from 'pkg'; +import * as rimraf from 'rimraf'; -const ROOT = path.join(__dirname, '..'); +export const ROOT = path.join(__dirname, '..'); -console.log('Building package...\n'); +/** + * Use the 'pkg' module to create a single large executable file with + * the contents of 'node_modules' and the CLI's javascript code. + * Also copy a number of native modules (binary '.node' files) that are + * compiled during 'npm install' to the 'build-bin' folder, alongside + * the single large executable file created by pkg. (This is necessary + * because of a pkg limitation that does not allow binary executables + * to be directly executed from inside another binary executable.) + */ +export async function buildPkg() { + console.log('Building package...\n'); -execPkg(['--target', 'host', '--output', 'build-bin/balena', 'package.json']) - .then(() => { - const xpaths: Array<[string, string[]]> = [ - // [platform, [path, to, file]] - ['*', ['opn', 'xdg-open']], - ['darwin', ['denymount', 'bin', 'denymount']], - ]; - return Bluebird.map(xpaths, ([platform, xpath]) => { - if (platform === '*' || platform === process.platform) { - // eg copy from node_modules/opn/xdg-open to build-bin/xdg-open - return fs.copy( - path.join(ROOT, 'node_modules', ...xpath), - path.join(ROOT, 'build-bin', xpath.pop()!), - ); - } - }).return(); - }) - .then(() => { - return filehound - .create() - .paths(path.join(ROOT, 'node_modules')) - .ext(['node', 'dll']) - .find(); - }) - .then(nativeExtensions => { - console.log(`\nCopying to build-bin:\n${nativeExtensions.join('\n')}`); - - return nativeExtensions.map(extPath => { + await execPkg([ + '--target', + 'host', + '--output', + 'build-bin/balena', + 'package.json', + ]); + const xpaths: Array<[string, string[]]> = [ + // [platform, [path, to, file]] + ['*', ['opn', 'xdg-open']], + ['darwin', ['denymount', 'bin', 'denymount']], + ]; + await Bluebird.map(xpaths, ([platform, xpath]) => { + if (platform === '*' || platform === process.platform) { + // eg copy from node_modules/opn/xdg-open to build-bin/xdg-open return fs.copy( - extPath, - extPath.replace( - path.join(ROOT, 'node_modules'), - path.join(ROOT, 'build-bin'), - ), + path.join(ROOT, 'node_modules', ...xpath), + path.join(ROOT, 'build-bin', xpath.pop()!), ); - }); + } }); + const nativeExtensionPaths: string[] = await filehound + .create() + .paths(path.join(ROOT, 'node_modules')) + .ext(['node', 'dll']) + .find(); + + console.log(`\nCopying to build-bin:\n${nativeExtensionPaths.join('\n')}`); + + await Bluebird.map(nativeExtensionPaths, extPath => + fs.copy( + extPath, + extPath.replace( + path.join(ROOT, 'node_modules'), + path.join(ROOT, 'build-bin'), + ), + ), + ); +} + +/** + * Run the `oclif-dev pack:win` or `pack:macos` command (depending on the value + * of process.platform) to generate the native installers (which end up under + * the 'dist' folder). There are some harcoded options such as selecting only + * 64-bit binaries under Windows. + */ +export async function buildOclifInstaller() { + console.log(`buildOclifInstaller cwd="${process.cwd()}" ROOT="${ROOT}"`); + let packOS = ''; + let packOpts = ['-r', ROOT]; + if (process.platform === 'darwin') { + packOS = 'macos'; + } else if (process.platform === 'win32') { + packOS = 'win'; + packOpts = packOpts.concat('-t', 'win32-x64'); + } + if (packOS) { + const packCmd = `pack:${packOS}`; + const dirs = [path.join(ROOT, 'dist', packOS)]; + if (packOS === 'win') { + dirs.push(path.join(ROOT, 'tmp', 'win*')); + } + for (const dir of dirs) { + console.log(`rimraf(${dir})`); + await Bluebird.fromCallback(cb => rimraf(dir, cb)); + } + console.log('======================================================='); + console.log(`oclif-dev "${packCmd}" [${packOpts}]`); + console.log('======================================================='); + oclifRun([packCmd].concat(...packOpts)); + } +} + +/** + * Convert e.g. 'C:\myfolder' -> '/C/myfolder' so that the path can be given + * as argument to "unix tools" like 'tar' under MSYS or MSYS2 on Windows. + */ +export function fixPathForMsys(p: string): string { + return p.replace(/\\/g, '/').replace(/^([a-zA-Z]):/, '/$1'); +} diff --git a/automation/deploy-bin.ts b/automation/deploy-bin.ts index 8201f8ae..15852ed7 100644 --- a/automation/deploy-bin.ts +++ b/automation/deploy-bin.ts @@ -15,70 +15,133 @@ * limitations under the License. */ import * as archiver from 'archiver'; -import * as Promise from 'bluebird'; +import * as Bluebird from 'bluebird'; import * as fs from 'fs-extra'; -import * as mkdirp from 'mkdirp'; -import * as os from 'os'; +import * as _ from 'lodash'; import * as path from 'path'; import * as publishRelease from 'publish-release'; -import * as packageJSON from '../package.json'; - -const publishReleaseAsync = Promise.promisify(publishRelease); -const mkdirpAsync = Promise.promisify(mkdirp); - const { GITHUB_TOKEN } = process.env; const ROOT = path.join(__dirname, '..'); - +// Note: the following 'tslint disable' line was only required to +// satisfy ts-node under Appveyor's MSYS2 on Windows -- oddly specific. +// Maybe something to do with '/' vs '\' in paths in some tslint file. +// tslint:disable-next-line:no-var-requires +const packageJSON = require(path.join(ROOT, 'package.json')); const version = 'v' + packageJSON.version; -const outputFile = path.join( - ROOT, - 'build-zip', - `balena-cli-${version}-${os.platform()}-${os.arch()}.zip`, -); +const arch = process.arch; -mkdirpAsync(path.dirname(outputFile)) - .then( - () => - new Promise((resolve, reject) => { - console.log('Zipping build...'); +function dPath(...paths: string[]) { + return path.join(ROOT, 'dist', ...paths); +} - const archive = archiver('zip', { - zlib: { level: 7 }, - }); - archive.directory(path.join(ROOT, 'build-bin'), 'balena-cli'); +interface PathByPlatform { + [platform: string]: string; +} - const outputStream = fs.createWriteStream(outputFile); +const standaloneZips: PathByPlatform = { + linux: dPath(`balena-cli-${version}-linux-${arch}-standalone.zip`), + darwin: dPath(`balena-cli-${version}-macOS-${arch}-standalone.zip`), + win32: dPath(`balena-cli-${version}-windows-${arch}-standalone.zip`), +}; - outputStream.on('close', resolve); - outputStream.on('error', reject); +const oclifInstallers: PathByPlatform = { + darwin: dPath('macos', `balena-${version}.pkg`), + win32: dPath('win', `balena-${version}-${arch}.exe`), +}; - archive.on('error', reject); - archive.on('warning', console.warn); +const renamedOclifInstallers: PathByPlatform = { + darwin: dPath(`balena-cli-${version}-macOS-${arch}-installer-BETA.pkg`), + win32: dPath(`balena-cli-${version}-windows-${arch}-installer-BETA.exe`), +}; - archive.pipe(outputStream); - archive.finalize(); - }), - ) - .then(() => { - console.log('Build zipped'); - console.log('Publishing build...'); +const finalReleaseAssets: { [platform: string]: string[] } = { + win32: [standaloneZips['win32'], renamedOclifInstallers['win32']], + darwin: [standaloneZips['darwin'], renamedOclifInstallers['darwin']], + linux: [standaloneZips['linux']], +}; - return publishReleaseAsync({ +/** + * Create the zip file for the standalone 'pkg' bundle previously created + * by the buildPkg() function in 'build-bin.ts'. + */ +export async function zipStandaloneInstaller() { + const outputFile = standaloneZips[process.platform]; + if (!outputFile) { + throw new Error( + `Standalone installer unavailable for platform "${process.platform}"`, + ); + } + await fs.mkdirp(path.dirname(outputFile)); + await new Bluebird((resolve, reject) => { + console.log(`Zipping build to "${outputFile}"...`); + + const archive = archiver('zip', { + zlib: { level: 7 }, + }); + archive.directory(path.join(ROOT, 'build-bin'), 'balena-cli'); + + const outputStream = fs.createWriteStream(outputFile); + + outputStream.on('close', resolve); + outputStream.on('error', reject); + + archive.on('error', reject); + archive.on('warning', console.warn); + + archive.pipe(outputStream); + archive.finalize(); + }); + console.log('Build zipped'); +} + +/** + * Create or update a release in GitHub's releases page, uploading the + * installer files (standalone zip + native oclif installers). + */ +export async function createGitHubRelease() { + console.log(`Publishing release ${version} to GitHub`); + const ghRelease = await Bluebird.fromCallback( + publishRelease.bind(null, { token: GITHUB_TOKEN || '', owner: 'balena-io', repo: 'balena-cli', tag: version, name: `balena-CLI ${version}`, reuseRelease: true, - assets: [outputFile], - }); - }) - .then(release => { - console.log(`Release ${version} successful: ${release.html_url}`); - }) - .catch(err => { + assets: finalReleaseAssets[process.platform], + }), + ); + console.log(`Release ${version} successful: ${ghRelease.html_url}`); +} + +/** + * Top-level function to create a CLI release in GitHub's releases page: + * call zipStandaloneInstaller(), rename the files as we'd like them to + * display on the releases page, and call createGitHubRelease() to upload + * the files. + */ +export async function release() { + console.log(`Creating release assets for CLI ${version}`); + try { + await zipStandaloneInstaller(); + } catch (error) { + console.log(`Error creating standalone installer zip file: ${error}`); + process.exit(1); + } + if (process.platform === 'win32' || process.platform === 'darwin') { + if (await fs.pathExists(oclifInstallers[process.platform])) { + await fs.rename( + oclifInstallers[process.platform], + renamedOclifInstallers[process.platform], + ); + } + } + try { + await createGitHubRelease(); + } catch (err) { console.error('Release failed'); console.error(err); process.exit(1); - }); + } +} diff --git a/automation/run.ts b/automation/run.ts new file mode 100644 index 00000000..08beb0e2 --- /dev/null +++ b/automation/run.ts @@ -0,0 +1,112 @@ +/** + * @license + * Copyright 2019 Balena Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { spawn } from 'child_process'; +import * as _ from 'lodash'; +import * as shellEscape from 'shell-escape'; + +import { + buildOclifInstaller, + buildPkg, + fixPathForMsys, + ROOT, +} from './build-bin'; +import { release } from './deploy-bin'; + +/** + * Run the MSYS2 bash.exe shell in a child process (child_process.spawn()). + * The given argv arguments are escaped using the 'shell-escape' package, + * so that backslashes in Windows paths, and other bash-special characters, + * are preserved. If argv is not provided, defaults to process.argv, to the + * effect that this current (parent) process is re-executed under MSYS2 bash. + * This is useful to change the default shell from cmd.exe to MSYS2 bash on + * Windows. + * @param argv Arguments to be shell-escaped and given to MSYS2 bash.exe. + */ +export async function runUnderMsys(argv?: string[]) { + const newArgv = argv || process.argv; + await new Promise((resolve, reject) => { + const cmd = 'C:\\msys64\\usr\\bin\\bash.exe'; + const args = ['-lc', shellEscape(newArgv)]; + const child = spawn(cmd, args, { stdio: 'inherit' }); + child.on('close', code => { + if (code) { + console.log(`runUnderMsys: child process exited with code ${code}`); + reject(code); + } else { + resolve(); + } + }); + }); +} + +/** + * Trivial command-line parser. Check whether the command-line argument is one + * of the following strings, then call the appropriate functions: + * 'build:installer' (to build a native oclif installer) + * 'build:standalone' (to build a standalone pkg package) + * 'release' (to create/update a GitHub release) + * + * In the case of 'build:installer', also call runUnderMsys() to switch the + * shell from cmd.exe to MSYS2 bash.exe. + * + * @param args Arguments to parse (default is process.argv.slice(2)) + */ +export async function run(args?: string[]) { + args = args || process.argv.slice(2); + console.log(`automation/run.ts process.argv=[${process.argv}]\n`); + console.log(`automation/run.ts args=[${args}]`); + if (_.isEmpty(args)) { + console.error('Error: missing args'); + process.exit(1); + } + const commands: { [cmd: string]: () => void } = { + 'build:installer': buildOclifInstaller, + 'build:standalone': buildPkg, + release, + }; + for (const arg of args) { + if (!commands.hasOwnProperty(arg)) { + throw new Error(`Error: unknown build target: ${arg}`); + } + } + + // If runUnderMsys() is called to re-execute this script under MSYS2, + // the current working dir becomes the MSYS2 homedir, so we change back. + process.chdir(ROOT); + + for (const arg of args) { + if (arg === 'build:installer' && process.platform === 'win32') { + // ensure running under MSYS2 + if (!process.env.MSYSTEM) { + process.env.MSYS2_PATH_TYPE = 'inherit'; + await runUnderMsys([ + fixPathForMsys(process.argv[0]), + fixPathForMsys(process.argv[1]), + arg, + ]); + continue; + } + if (process.env.MSYS2_PATH_TYPE !== 'inherit') { + throw new Error('the MSYS2_PATH_TYPE env var must be set to "inherit"'); + } + } + await commands[arg](); + } +} + +run(); diff --git a/automation/tsconfig.json b/automation/tsconfig.json index 99131ce4..83d916dc 100644 --- a/automation/tsconfig.json +++ b/automation/tsconfig.json @@ -8,6 +8,7 @@ "noUnusedParameters": true, "preserveConstEnums": true, "removeComments": true, + "resolveJsonModule": true, "sourceMap": true, "skipLibCheck": true, "typeRoots" : [ diff --git a/bin/balena b/bin/run similarity index 100% rename from bin/balena rename to bin/run diff --git a/package.json b/package.json index c7699040..6b13325b 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,13 @@ }, "preferGlobal": true, "files": [ - "bin/balena", + "bin/run", "build/", "doc/", "lib/" ], "bin": { - "balena": "./bin/balena" + "balena": "./bin/run" }, "pkg": { "scripts": [ @@ -31,13 +31,15 @@ ] }, "scripts": { - "prebuild": "rimraf build/ build-bin/ build-zip/ && patch-package", - "build": "npm run build:src && npm run build:bin", + "postinstall": "patch-package", + "prebuild": "rimraf build/ build-bin/", + "build": "npm run build:src", "build:src": "npm run prettify && npm run lint && npm run build:fast && npm run build:doc", "build:fast": "gulp build && tsc", "build:doc": "mkdirp doc/ && ts-node --type-check -P automation/tsconfig.json automation/capitanodoc/index.ts > doc/cli.markdown", - "build:bin": "ts-node --type-check -P automation/tsconfig.json automation/build-bin.ts", - "release": "npm run build && ts-node --type-check -P automation/tsconfig.json automation/deploy-bin.ts", + "build:standalone": "ts-node --type-check -P automation/tsconfig.json automation/run.ts build:standalone", + "build:installer": "ts-node --type-check -P automation/tsconfig.json automation/run.ts build:installer", + "release": "ts-node --type-check -P automation/tsconfig.json automation/run.ts release", "pretest": "npm run build", "test": "gulp test", "test:fast": "npm run build:fast && gulp test", @@ -45,7 +47,6 @@ "watch": "gulp watch", "prettify": "prettier --write \"{lib,tests,automation,typings}/**/*.ts\" --config ./node_modules/resin-lint/config/.prettierrc", "lint": "resin-lint lib/ tests/ && resin-lint --typescript automation/ lib/ typings/ tests/", - "prepublish": "require-npm4-to-publish", "prepublishOnly": "npm run build" }, "keywords": [ @@ -70,15 +71,15 @@ } }, "devDependencies": { - "@oclif/dev-cli": "^1.22.0", "@oclif/config": "^1.12.12", + "@oclif/dev-cli": "1.22.0", "@oclif/parser": "^3.7.3", "@types/archiver": "2.1.2", "@types/bluebird": "3.5.21", "@types/chokidar": "^1.7.5", "@types/common-tags": "1.4.0", "@types/dockerode": "2.5.5", - "@types/fs-extra": "5.0.4", + "@types/fs-extra": "7.0.0", "@types/is-root": "1.0.0", "@types/lodash": "4.14.112", "@types/mixpanel": "2.14.0", @@ -89,13 +90,15 @@ "@types/prettyjson": "0.0.28", "@types/raven": "2.5.1", "@types/request": "2.48.1", + "@types/rimraf": "^2.0.2", + "@types/shell-escape": "^0.2.0", "@types/stream-to-promise": "2.2.0", "@types/tar-stream": "1.6.0", "@types/through2": "2.0.33", "catch-uncommitted": "^1.3.0", "ent": "^2.2.0", "filehound": "^1.17.0", - "fs-extra": "^5.0.0", + "fs-extra": "^8.0.1", "gulp": "^4.0.1", "gulp-coffee": "^2.2.0", "gulp-inline-source": "^2.1.0", @@ -105,10 +108,10 @@ "patch-package": "^6.1.2", "pkg": "~4.3.8", "prettier": "^1.17.0", - "publish-release": "^1.3.3", - "require-npm4-to-publish": "^1.0.0", + "publish-release": "^1.6.0", "resin-lint": "^3.0.1", "rewire": "^3.0.2", + "shell-escape": "^0.2.0", "ts-node": "^8.1.0", "typescript": "3.4.3" }, @@ -165,7 +168,7 @@ "mkdirp": "^0.5.1", "moment": "^2.24.0", "moment-duration-format": "~2.2.2", - "mz": "^2.6.0", + "mz": "^2.7.0", "node-cleanup": "^2.1.2", "oclif": "^1.13.1", "opn": "^5.5.0", diff --git a/patches/@oclif+dev-cli+1.22.0.patch b/patches/@oclif+dev-cli+1.22.0.patch new file mode 100644 index 00000000..d4572bb0 --- /dev/null +++ b/patches/@oclif+dev-cli+1.22.0.patch @@ -0,0 +1,146 @@ +diff --git a/node_modules/@oclif/dev-cli/lib/commands/pack/win.js b/node_modules/@oclif/dev-cli/lib/commands/pack/win.js +index a9d4276..75c2f8b 100644 +--- a/node_modules/@oclif/dev-cli/lib/commands/pack/win.js ++++ b/node_modules/@oclif/dev-cli/lib/commands/pack/win.js +@@ -3,11 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true }); + const command_1 = require("@oclif/command"); + const qq = require("qqjs"); + const Tarballs = require("../../tarballs"); ++const { fixPath } = require("../../util"); ++ + class PackWin extends command_1.Command { + async run() { + await this.checkForNSIS(); + const { flags } = this.parse(PackWin); +- const buildConfig = await Tarballs.buildConfig(flags.root); ++ const targets = flags.targets !== undefined ? flags.targets.split(',') : undefined; ++ const buildConfig = await Tarballs.buildConfig(flags.root, {targets}); + const { config } = buildConfig; + await Tarballs.build(buildConfig, { platform: 'win32', pack: false }); + const arches = buildConfig.targets.filter(t => t.platform === 'win32').map(t => t.arch); +@@ -17,7 +20,7 @@ class PackWin extends command_1.Command { + await qq.write([installerBase, `bin/${config.bin}`], scripts.sh(config)); + await qq.write([installerBase, `${config.bin}.nsi`], scripts.nsis(config, arch)); + await qq.mv(buildConfig.workspace({ platform: 'win32', arch }), [installerBase, 'client']); +- await qq.x(`makensis ${installerBase}/${config.bin}.nsi | grep -v "\\[compress\\]" | grep -v "^File: Descending to"`); ++ await qq.x(`makensis ${fixPath(installerBase)}/${config.bin}.nsi | grep -v "\\[compress\\]" | grep -v "^File: Descending to"`) + const o = buildConfig.dist(`win/${config.bin}-v${buildConfig.version}-${arch}.exe`); + await qq.mv([installerBase, 'installer.exe'], o); + this.log(`built ${o}`); +@@ -40,6 +43,7 @@ class PackWin extends command_1.Command { + PackWin.description = 'create windows installer from oclif CLI'; + PackWin.flags = { + root: command_1.flags.string({ char: 'r', description: 'path to oclif CLI root', default: '.', required: true }), ++ targets: command_1.flags.string({char: 't', description: 'comma-separated targets to pack (e.g.: win32-x86,win32-x64)'}), + }; + exports.default = PackWin; + const scripts = { +diff --git a/node_modules/@oclif/dev-cli/lib/tarballs/build.js b/node_modules/@oclif/dev-cli/lib/tarballs/build.js +index 3e613e0..4ed799c 100644 +--- a/node_modules/@oclif/dev-cli/lib/tarballs/build.js ++++ b/node_modules/@oclif/dev-cli/lib/tarballs/build.js +@@ -19,6 +19,9 @@ const pack = async (from, to) => { + async function build(c, options = {}) { + const { xz, config } = c; + const prevCwd = qq.cwd(); ++ ++ console.log(`[patched @oclif/dev-cli] cwd="${prevCwd}"\n c.root="${c.root}" c.workspace()="${c.workspace()}"`); ++ + const packCLI = async () => { + const stdout = await qq.x.stdout('npm', ['pack', '--unsafe-perm'], { cwd: c.root }); + return path.join(c.root, stdout.split('\n').pop()); +@@ -34,6 +37,21 @@ async function build(c, options = {}) { + await qq.mv(f, '.'); + await qq.rm('package', tarball, 'bin/run.cmd'); + }; ++ const copyCLI = async() => { ++ const ws = c.workspace(); ++ await qq.emptyDir(ws); ++ qq.cd(ws); ++ const sources = [ ++ 'bin', 'build', 'patches', 'typings', 'CHANGELOG.md', 'INSTALL.md', ++ 'LICENSE', 'package.json', 'package-lock.json', 'README.md', ++ 'TROUBLESHOOTING.md', ++ ]; ++ for (const source of sources) { ++ console.log(`cp "${source}" -> "${ws}"`); ++ await qq.cp(path.join(c.root, source), ws); ++ } ++ await qq.rm('bin/run.cmd'); ++ } + const updatePJSON = async () => { + qq.cd(c.workspace()); + const pjson = await qq.readJSON('package.json'); +@@ -124,7 +142,8 @@ async function build(c, options = {}) { + await qq.writeJSON(c.dist(config.s3Key('manifest')), manifest); + }; + log_1.log(`gathering workspace for ${config.bin} to ${c.workspace()}`); +- await extractCLI(await packCLI()); ++ // await extractCLI(await packCLI()); ++ await copyCLI(); + await updatePJSON(); + await addDependencies(); + await bin_1.writeBinScripts({ config, baseWorkspace: c.workspace(), nodeVersion: c.nodeVersion }); +diff --git a/node_modules/@oclif/dev-cli/lib/tarballs/node.js b/node_modules/@oclif/dev-cli/lib/tarballs/node.js +index 343eb00..7df1815 100644 +--- a/node_modules/@oclif/dev-cli/lib/tarballs/node.js ++++ b/node_modules/@oclif/dev-cli/lib/tarballs/node.js +@@ -1,9 +1,11 @@ + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + const errors_1 = require("@oclif/errors"); ++const { isMSYS2 } = require('execa'); + const path = require("path"); + const qq = require("qqjs"); + const log_1 = require("../log"); ++const { fixPath } = require("../util"); + async function checkFor7Zip() { + try { + await qq.x('7z', { stdio: [0, null, 2] }); +@@ -40,7 +42,8 @@ async function fetchNodeBinary({ nodeVersion, output, platform, arch, tmp }) { + const basedir = path.dirname(tarball); + await qq.mkdirp(basedir); + await qq.download(url, tarball); +- await qq.x(`grep ${path.basename(tarball)} ${shasums} | shasum -a 256 -c -`, { cwd: basedir }); ++ const shaCmd = isMSYS2 ? 'sha256sum -c -' : 'shasum -a 256 -c -'; ++ await qq.x(`grep ${path.basename(tarball)} ${fixPath(shasums)} | ${shaCmd}`, { cwd: basedir }); + }; + const extract = async () => { + log_1.log(`extracting ${nodeBase}`); +@@ -50,7 +53,7 @@ async function fetchNodeBinary({ nodeVersion, output, platform, arch, tmp }) { + await qq.mkdirp(path.dirname(cache)); + if (platform === 'win32') { + qq.pushd(nodeTmp); +- await qq.x(`7z x -bd -y ${tarball} > /dev/null`); ++ await qq.x(`7z x -bd -y ${fixPath(tarball)} > /dev/null`); + await qq.mv([nodeBase, 'node.exe'], cache); + qq.popd(); + } +diff --git a/node_modules/@oclif/dev-cli/lib/util.js b/node_modules/@oclif/dev-cli/lib/util.js +index 17368b4..7766d88 100644 +--- a/node_modules/@oclif/dev-cli/lib/util.js ++++ b/node_modules/@oclif/dev-cli/lib/util.js +@@ -1,5 +1,6 @@ + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); ++const { isCygwin, isMinGW, isMSYS2 } = require('execa'); + const _ = require("lodash"); + function castArray(input) { + if (input === undefined) +@@ -40,3 +41,16 @@ function sortBy(arr, fn) { + } + exports.sortBy = sortBy; + exports.template = (context) => (t) => _.template(t || '')(context); ++ ++function fixPath(badPath) { ++ // 'c:\myfolder' -> '/c/myfolder' or '/cygdrive/c/myfolder' ++ let fixed = badPath.replace(/\\/g, '/'); ++ if (isMSYS2 || isMinGW) { ++ fixed = fixed.replace(/^([a-zA-Z]):/, '/$1'); ++ } else if (isCygwin) { ++ fixed = fixed.replace(/^([a-zA-Z]):/, '/cygdrive/$1'); ++ } ++ console.log(`[patched @oclif/dev-cli] fixPath before="${badPath}" after="${fixed}"`); ++ return fixed; ++} ++exports.fixPath = fixPath; diff --git a/patches/qqjs++execa+0.10.0.patch b/patches/qqjs++execa+0.10.0.patch new file mode 100644 index 00000000..1024c3ae --- /dev/null +++ b/patches/qqjs++execa+0.10.0.patch @@ -0,0 +1,49 @@ +diff --git a/node_modules/qqjs/node_modules/execa/index.js b/node_modules/qqjs/node_modules/execa/index.js +index 06f3969..6251e17 100644 +--- a/node_modules/qqjs/node_modules/execa/index.js ++++ b/node_modules/qqjs/node_modules/execa/index.js +@@ -14,6 +14,21 @@ const stdio = require('./lib/stdio'); + + const TEN_MEGABYTES = 1000 * 1000 * 10; + ++// OSTYPE is 'msys' for MSYS 1.0 and for MSYS2, or 'cygwin' for Cygwin ++// but note that OSTYPE is not "exported" by default, so run: export OSTYPE=$OSTYPE ++// MSYSTEM is 'MINGW32' for MSYS 1.0, 'MSYS' for MSYS2, and undefined for Cygwin ++const isCygwin = process.env.OSTYPE === 'cygwin' ++const isMinGW = process.env.MSYSTEM && process.env.MSYSTEM.startsWith('MINGW') ++const isMSYS2 = process.env.MSYSTEM && process.env.MSYSTEM.startsWith('MSYS') ++ ++exports.isCygwin = isCygwin ++exports.isMinGW = isMinGW ++exports.isMSYS2 = isMSYS2 ++ ++console.log(`[patched execa] detected "${ ++ isCygwin ? 'Cygwin' : isMinGW ? 'MinGW' : isMSYS2 ? 'MSYS2' : 'standard' ++}" environment (MSYSTEM="${process.env.MSYSTEM}")`) ++ + function handleArgs(cmd, args, opts) { + let parsed; + +@@ -104,13 +119,21 @@ function handleShell(fn, cmd, opts) { + + opts = Object.assign({}, opts); + +- if (process.platform === 'win32') { ++ if (isMSYS2 || isMinGW || isCygwin) { ++ file = process.env.MSYSSHELLPATH || ++ (isMSYS2 ? 'C:\\msys64\\usr\\bin\\bash.exe' : ++ (isMinGW ? 'C:\\MinGW\\msys\\1.0\\bin\\bash.exe' : ++ (isCygwin ? 'C:\\cygwin64\\bin\\bash.exe' : file))); ++ } ++ else if (process.platform === 'win32') { + opts.__winShell = true; + file = process.env.comspec || 'cmd.exe'; + args = ['/s', '/c', `"${cmd}"`]; + opts.windowsVerbatimArguments = true; + } + ++ console.log(`[patched execa] handleShell file="${file}" args="[${args}]"`); ++ + if (opts.shell) { + file = opts.shell; + delete opts.shell;