Skip to content
This repository has been archived by the owner on Apr 21, 2022. It is now read-only.

Commit

Permalink
feat: pack
Browse files Browse the repository at this point in the history
  • Loading branch information
jdx committed Apr 7, 2018
1 parent 34c6411 commit b6b11bb
Show file tree
Hide file tree
Showing 6 changed files with 491 additions and 48 deletions.
17 changes: 8 additions & 9 deletions .circleci/config.yml
Expand Up @@ -3,7 +3,7 @@ version: 2
jobs:
node-latest: &test
docker:
- image: node:latest
- image: oclif/release:0.0.0-9.11.1
working_directory: ~/cli
environment:
NYC: "yarn exec nyc -- --nycrc-path node_modules/@oclif/nyc-config/.nycrc"
Expand All @@ -12,38 +12,37 @@ jobs:
- checkout
- restore_cache: &restore_cache
keys:
- v2-yarn-{{checksum ".circleci/config.yml"}}-{{ checksum "yarn.lock"}}
- v2-yarn-{{checksum ".circleci/config.yml"}}
- v3-yarn-{{checksum ".circleci/config.yml"}}-{{ checksum "yarn.lock"}}
- v3-yarn-{{checksum ".circleci/config.yml"}}
- run: .circleci/greenkeeper
- run: yarn add -D nyc@11 @oclif/nyc-config@1 mocha-junit-reporter@1 @commitlint/cli@6 @commitlint/config-conventional@6
- run: yarn add -D nyc@11 @oclif/nyc-config@1 mocha-junit-reporter@1
- run: yarn run build
- run: ./bin/run -v
- run: ./bin/run --version
- run: ./bin/run --help
- run: |
mkdir -p reports
$NYC yarn test --reporter mocha-junit-reporter
$NYC report --reporter text-lcov > coverage.lcov
curl -s https://codecov.io/bash | bash
- run: yarn exec commitlint -- -x @commitlint/config-conventional --from origin/master
- store_test_results: &store_test_results
path: ~/cli/reports
node-8:
<<: *test
docker:
- image: node:8
- image: oclif/release:0.0.0-8.11.1
release:
<<: *test
steps:
- add_ssh_keys
- checkout
- restore_cache: *restore_cache
- run: yarn global add @oclif/semantic-release@1 semantic-release@12
- run: yarn global add @oclif/semantic-release@2 semantic-release@15
- run: yarn --frozen-lockfile
- run: |
export PATH=/usr/local/share/.config/yarn/global/node_modules/.bin:$PATH
semantic-release -e @oclif/semantic-release
- save_cache:
key: v2-yarn-{{checksum ".circleci/config.yml"}}-{{checksum "yarn.lock"}}
key: v3-yarn-{{checksum ".circleci/config.yml"}}-{{checksum "yarn.lock"}}
paths:
- ~/cli/node_modules
- /usr/local/share/.cache/yarn
Expand Down
18 changes: 12 additions & 6 deletions package.json
Expand Up @@ -8,28 +8,34 @@
},
"bugs": "https://github.com/oclif/dev-cli/issues",
"dependencies": {
"@heroku-cli/color": "^1.1.3",
"@oclif/command": "^1.4.7",
"@oclif/config": "^1.3.62",
"@oclif/config": "^1.3.64",
"@oclif/errors": "^1.0.3",
"@oclif/plugin-help": "^1.2.2",
"@oclif/plugin-warn-if-update-available": "^1.2.3",
"@oclif/plugin-help": "^1.2.3",
"@oclif/plugin-warn-if-update-available": "^1.2.4",
"cli-ux": "^3.3.27",
"fs-extra": "^5.0.0",
"lodash": "^4.17.5",
"lodash.template": "^4.4.0",
"normalize-package-data": "^2.4.0",
"qqjs": "^0.1.0",
"require-resolve": "^0.0.2"
},
"devDependencies": {
"@oclif/plugin-legacy": "^1.0.7",
"@oclif/test": "^1.0.4",
"@oclif/tslint": "^1.1.0",
"@types/chai": "^4.1.2",
"@types/execa": "^0.9.0",
"@types/fs-extra": "^5.0.1",
"@types/globby": "^6.1.0",
"@types/lodash": "^4.14.106",
"@types/lodash.template": "^4.4.3",
"@types/mocha": "^5.0.0",
"@types/node": "^9.6.2",
"@types/write-json-file": "^2.2.1",
"chai": "^4.1.2",
"fs-extra": "^5.0.0",
"globby": "^8.0.1",
"mocha": "^5.0.5",
"ts-node": "^5.0.1",
Expand Down Expand Up @@ -65,8 +71,8 @@
"postpublish": "rm .oclif.manifest.json",
"posttest": "yarn run lint",
"prepublishOnly": "yarn run build && node ./bin/run manifest && node ./bin/run readme",
"version": "node ./bin/run manifest && node ./bin/run readme && git add README.md",
"test": "mocha --forbid-only \"test/**/*.test.ts\""
"test": "mocha --forbid-only \"test/**/*.test.ts\"",
"version": "node ./bin/run manifest && node ./bin/run readme && git add README.md"
},
"types": "lib/index.d.ts"
}
256 changes: 256 additions & 0 deletions src/commands/pack.ts
@@ -0,0 +1,256 @@
import {Command, flags} from '@oclif/command'
import * as Config from '@oclif/config'
import ux from 'cli-ux'
import * as path from 'path'
import * as qq from 'qqjs'

export default class Manifest extends Command {
static description = `
packages oclif cli into tarballs
This can be used to create oclif CLIs that use the system node or that come preloaded with a node binary.
The default output will be ./dist/mycli-v0.0.0.tar.gz for tarballs without node or ./dist/mycli-v0.0.0-darwin-x64.tar.gz when node is included.
By default it will not include node. To include node, pass in the --platform and --arch flags.
`
static examples = [
`$ oclif-dev pack
outputs tarball of CLI in current directory to ./dist/mycli-v0.0.0.tar.gz`,
`$ oclif-dev pack --platform win32 --arch x64
outputs tarball of CLI including a windows-x64 binary to ./dist/mycli-v0.0.0-win32-x64.tar.gz`,
]

static flags = {
output: flags.string({char: 'o', description: 'output location'}),
root: flags.string({char: 'r', description: 'path to oclif CLI root', default: '.', required: true}),
'node-version': flags.string({description: 'node version of binary to get', default: process.versions.node, required: true}),
platform: flags.string({char: 'p', description: 'OS to use for node binary', options: ['darwin', 'linux', 'win32']}),
arch: flags.string({char: 'a', description: 'arch to use for node binary', options: ['x64', 'x86', 'arm']}),
channel: flags.string({char: 'c', description: 'channel to publish (e.g. "stable" or "beta")', required: true}),
}

cli!: Config.IConfig
root!: string
workspace!: string
channel!: string
version!: string
output!: string
platform?: string
arch?: string
_tmp?: string
_base?: string

get tmp() {
if (this._tmp) return this._tmp
this._tmp = path.join(this.root, 'tmp')
qq.mkdirp.sync(this._tmp)
return this._tmp
}

get base() {
if (this._base) return this._base
this._base = [this.config.bin, `v${this.config.version}`].join('-')
if (this.platform) {
this._base = [this._base, this.platform, this.arch].join('-')
}
return this._base
}

async run() {
const prevCwd = qq.cwd()
if (process.platform === 'win32') throw new Error('pack does not function on windows')
const {flags} = this.parse(Manifest)
this.channel = flags.channel
this.root = path.resolve(flags.root)
qq.cd(this.root)
this.platform = flags.platform
this.arch = flags.arch
if (this.platform && !this.arch) throw new Error('--platform and --arch must be specified together')

this.cli = await Config.load({root: this.root, devPlugins: false, userPlugins: false})
this.version = await this.getVersion()
this.workspace = path.join(this.tmp, this.base)

this.output = flags.output ? path.resolve(flags.output) : path.join(process.cwd(), 'dist', `${this.base}.tar.gz`)
ux.action.start(`packing ${this.config.bin} to ${this.output}`)

await qq.emptyDir(this.workspace)
ux.action.start(`packing ${this.config.bin} to ${this.output}`)
await this.buildCLI()
await this.writeBinScripts()
if (this.platform && this.arch) {
await this.downloadNode(flags['node-version'], path.join(this.workspace, 'bin', this.platform === 'win32' ? 'node.exe' : 'node'))
}
await this.tarballize()
qq.cd(prevCwd)
}

async getVersion() {
if (!await qq.exists('.git')) return this.cli.version
// add git sha to version if in a git repo
qq.pushd(this.root)
const sha = (await qq.x.stdout('git', ['rev-parse', '--short', 'HEAD']))
qq.popd()
return `${this.cli.version}-${sha}`
}

async buildCLI() {
const packCLI = () => {
qq.cd(this.root)
return qq.x.stdout('npm', ['pack'])
}
const extractCLI = async (tarball: string) => {
await qq.mv(tarball, this.workspace)
tarball = qq.join([this.workspace, tarball])
qq.cd(this.workspace)
await qq.x(`tar -xzf ${tarball}`)
for (let f of await qq.ls('package', {fullpath: true})) await qq.mv(f, '.')
await qq.rm('package', tarball, 'bin/run.cmd')
}
const updatePJSON = async () => {
qq.cd(this.workspace)
const pjson = await qq.readJSON('package.json')
pjson.version = this.version
pjson.channel = this.channel
await qq.writeJSON('package.json', pjson)
}
const addDependencies = async () => {
qq.cd(this.workspace)
const yarn = await qq.exists.sync([this.root, 'yarn.lock'])
if (yarn) {
await qq.cp([this.root, 'yarn.lock'], '.')
await qq.x('yarn --no-progress --production --non-interactive')
} else {
await qq.cp([this.root, 'package-lock.json'], '.')
await qq.x('npm install --production')
}
}
const prune = async () => {
// removes unnecessary files to make the tarball smaller
qq.cd(this.workspace)
const toRemove = await qq.globby([
'node_modules/**/README*',
'**/CHANGELOG*',
'**/*.ts',
], {nocase: true})
await qq.rm(...toRemove)
await qq.rmIfEmpty('.')
}
await extractCLI(await packCLI())
await updatePJSON()
await addDependencies()
await prune()
}

async downloadNode(nodeVersion: string, output: string) {
const nodeBase = this.platform === 'win32'
? `node-v${nodeVersion}-win-${this.arch}`
: `node-v${nodeVersion}-${this.platform}-${this.arch}`
const tarball = path.join(this.tmp, 'cache', this.platform === 'win32' ? `${nodeBase}.7z` : `${nodeBase}.tar.xz`)
const download = async () => {
ux.action.start(`downloading ${nodeBase}`)
await qq.mkdirp(path.dirname(tarball))
const url = this.platform === 'win32'
? `https://nodejs.org/dist/v${nodeVersion}/${nodeBase}.7z`
: `https://nodejs.org/dist/v${nodeVersion}/${nodeBase}.tar.xz`
await qq.download(url, tarball)
ux.action.stop()
}
const extract = async () => {
ux.action.start(`extracting ${nodeBase}`)
const nodeTmp = path.join(this.tmp, 'node')
await qq.emptyDir(nodeTmp)
await qq.mkdirp(path.dirname(output))
if (this.platform === 'win32') {
qq.pushd(nodeTmp)
await qq.x(`7z x -bd -y ${tarball} > /dev/null`)
await qq.mv([nodeBase, 'node.exe'], output)
qq.popd()
} else {
await qq.x(`tar -C ${this.tmp}/node -xJf ${tarball}`)
await qq.mv([nodeTmp, nodeBase, 'bin/node'], output)
}
await qq.rm([nodeTmp, nodeBase])
await qq.rmIfEmpty(nodeTmp)
ux.action.stop()
}
if (!await qq.exists(tarball)) await download()
await extract()
}

async writeBinScripts() {
ux.action.start('writing bin scripts')
const binPathEnvVar = this.cli.scopedEnvVarKey('CLI_BINPATH')
const redirectedEnvVar = this.cli.scopedEnvVarKey('CLI_REDIRECTED')
if (this.platform === 'win32') {
const node = this.platform ? '"%~dp0\\..\\client\\bin\\node.exe"' : 'node'
await qq.write([this.workspace, 'bin', `${this.cli.bin}.cmd`], `@echo off
if exist "%LOCALAPPDATA%\\${this.cli.dirname}\\client\\bin\\${this.cli.bin}.cmd" (
set ${redirectedEnvVar}=1
"%LOCALAPPDATA%\\${this.cli.dirname}\\client\\bin\\${this.cli.bin}.cmd" %*
) else (
set ${binPathEnvVar}="%~dp0\\${this.cli.bin}.cmd"
${node} "%~dp0\\..\\client\\bin\\run" %*
)
`)
await qq.write([this.workspace, 'bin', this.cli.bin], `#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
"$basedir/../client/bin/${this.cli.bin}.cmd" "$@"
ret=$?
exit $ret
`)
} else {
const bin = qq.join([this.workspace, 'bin', this.cli.bin])
const node = this.platform ? '"\$DIR/node"' : 'node'
await qq.write(bin, `#!/usr/bin/env bash
set -e
get_script_dir () {
SOURCE="\${BASH_SOURCE[0]}"
while [ -h "\$SOURCE" ]; do
DIR="\$( cd -P "\$( dirname "\$SOURCE" )" && pwd )"
SOURCE="\$( readlink "\$SOURCE" )"
[[ \$SOURCE != /* ]] && SOURCE="\$DIR/\$SOURCE"
done
DIR="\$( cd -P "\$( dirname "\$SOURCE" )" && pwd )"
echo "\$DIR"
}
DIR=\$(get_script_dir)
CLI_HOME=\$(cd && pwd)
XDG_DATA_HOME=\${XDG_DATA_HOME:="\$CLI_HOME/.local/share"}
BIN_PATH="\$XDG_DATA_HOME/${this.cli.dirname}/client/bin/${this.cli.bin}"
if [ -z "\$${redirectedEnvVar}" ] && [ -x "\$BIN_PATH" ] && [[ ! "\$DIR/${this.cli.bin}" -ef "\$BIN_PATH" ]]; then
if [ "\$DEBUG" == "*" ]; then
echo "\$BIN_PATH" "\$@"
fi
"\$BIN_PATH" "\$@"
else
if [ "\$DEBUG" == "*" ]; then
echo ${binPathEnvVar}="\$DIR/${this.config.bin}" ${node} "\$DIR/run" "\$@"
fi
${binPathEnvVar}="\$DIR/${this.config.bin}" ${node} "\$DIR/run" "\$@"
fi
`)
await qq.chmod(bin, 0o755)
}
ux.action.stop()
}

async tarballize() {
ux.action.start('packing tarball')
qq.cd(this.tmp)
await qq.mkdirp(path.dirname(this.output))

// move the directory so we can get a friendlier name in the tarball
const tarTmp = path.join(this.tmp, this.base + '-tartmp')
await qq.emptyDir(tarTmp)
await qq.mv(this.base, [tarTmp, this.cli.bin])
qq.cd(tarTmp)

await qq.x('tar', ['czf', this.output, this.cli.bin])

await qq.rm(tarTmp)
qq.cd(this.tmp)
ux.action.stop()
}
}
20 changes: 20 additions & 0 deletions test/commands/pack.test.ts
@@ -0,0 +1,20 @@
import {expect, test} from '@oclif/test'
import * as qq from 'qqjs'

describe('pack', () => {
beforeEach(async () => {
await qq.rm('tmp/tarballs')
})

test
.command(['pack', '-pwin32', '-ax64', '-cdev', '-otmp/tarballs/win32-x64.tar.gz'])
.it('packs win32-64', () => {
expect(qq.exists.sync('tmp/tarballs/win32-x64.tar.gz')).to.be.true
})

test
.command(['pack', '-plinux', '-ax86', '-cdev', '-otmp/tarballs/linux-x86.tar.gz'])
.it('packs linux-x86', () => {
expect(qq.exists.sync('tmp/tarballs/linux-x86.tar.gz')).to.be.true
})
})
2 changes: 1 addition & 1 deletion test/mocha.opts
Expand Up @@ -4,4 +4,4 @@
--watch-extensions ts
--recursive
--reporter spec
--timeout 5000
--timeout 0

0 comments on commit b6b11bb

Please sign in to comment.