diff --git a/.gitignore b/.gitignore index 3c3629e..0b70e9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ node_modules +npm-debug.log +sls-sample-app-2.0.3.tgz +_suite +_onto diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2654cd3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,187 @@ +### Contributing ### + +Thank you for your interest in strong-build, an open source project +administered by StrongLoop. + +Contributing to strong-build is easy. In a few simple steps: + + * Ensure that your effort is aligned with the project's roadmap by + talking to the maintainers, especially if you are going to spend a + lot of time on it. + + * Make something better or fix a bug. + + * Adhere to code style outlined in the [Google C++ Style Guide][] and + [Google Javascript Style Guide][]. + + * [Sign your patches](#signing-patches) to indicate that your are + making your contribution available under the terms of the + [Contributor License Agreement](#contributor-license-agreement). + + * Submit a pull request through Github. + + +### Signing patches ### + +Like many open source projects, we need a contributor license agreement +from you before we can merge in your changes. + +In summary, by submitting your code, you are granting us a right to use +that code under the terms of this Agreement, including providing it to +others. You are also certifying that you wrote it, and that you are +allowed to license it to us. You are not giving up your copyright in +your work. The license does not change your rights to use your own +contributions for any other purpose. + +Contributor License Agreements are important because they define the +chain of ownership of a piece of software. Some companies won't allow +the use of free software without clear agreements around code ownership. +That's why many open source projects collect similar agreements from +contributors. The CLA here is based on the Apache CLA. + +To signify your agreement to these terms, add the following line to the +bottom of your commit message. Use your real name and an actual e-mail +address. + +``` +Signed-off-by: Random J Developer +``` + +Alternatively you can use the git command line to automatically add this +line, as follows: + +``` +$ git commit -sm "Replace rainbows by unicorns" +``` + + +### Contributor License Agreement ### + +``` + Individual Contributor License Agreement + + By signing this Individual Contributor License Agreement + ("Agreement"), and making a Contribution (as defined below) to + StrongLoop, Inc. ("StrongLoop"), You (as defined below) accept and + agree to the following terms and conditions for Your present and + future Contributions submitted to StrongLoop. Except for the license + granted in this Agreement to StrongLoop and recipients of software + distributed by StrongLoop, You reserve all right, title, and interest + in and to Your Contributions. + + 1. Definitions + + "You" or "Your" shall mean the copyright owner or the individual + authorized by the copyright owner that is entering into this + Agreement with StrongLoop. + + "Contribution" shall mean any original work of authorship, + including any modifications or additions to an existing work, that + is intentionally submitted by You to StrongLoop for inclusion in, + or documentation of, any of the products owned or managed by + StrongLoop ("Work"). For purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication + sent to StrongLoop or its representatives, including but not + limited to communication or electronic mailing lists, source code + control systems, and issue tracking systems that are managed by, + or on behalf of, StrongLoop for the purpose of discussing and + improving the Work, but excluding communication that is + conspicuously marked or otherwise designated in writing by You as + "Not a Contribution." + + 2. You Grant a Copyright License to StrongLoop + + Subject to the terms and conditions of this Agreement, You hereby + grant to StrongLoop and recipients of software distributed by + StrongLoop, a perpetual, worldwide, non-exclusive, no-charge, + royalty-free, irrevocable copyright license to reproduce, prepare + derivative works of, publicly display, publicly perform, + sublicense, and distribute Your Contributions and such derivative + works under any license and without any restrictions. + + 3. You Grant a Patent License to StrongLoop + + Subject to the terms and conditions of this Agreement, You hereby + grant to StrongLoop and to recipients of software distributed by + StrongLoop a perpetual, worldwide, non-exclusive, no-charge, + royalty-free, irrevocable (except as stated in this Section) + patent license to make, have made, use, offer to sell, sell, + import, and otherwise transfer the Work under any license and + without any restrictions. The patent license You grant to + StrongLoop under this Section applies only to those patent claims + licensable by You that are necessarily infringed by Your + Contributions(s) alone or by combination of Your Contributions(s) + with the Work to which such Contribution(s) was submitted. If any + entity institutes a patent litigation against You or any other + entity (including a cross-claim or counterclaim in a lawsuit) + alleging that Your Contribution, or the Work to which You have + contributed, constitutes direct or contributory patent + infringement, any patent licenses granted to that entity under + this Agreement for that Contribution or Work shall terminate as + of the date such litigation is filed. + + 4. You Have the Right to Grant Licenses to StrongLoop + + You represent that You are legally entitled to grant the licenses + in this Agreement. + + If Your employer(s) has rights to intellectual property that You + create, You represent that You have received permission to make + the Contributions on behalf of that employer, that Your employer + has waived such rights for Your Contributions, or that Your + employer has executed a separate Corporate Contributor License + Agreement with StrongLoop. + + 5. The Contributions Are Your Original Work + + You represent that each of Your Contributions are Your original + works of authorship (see Section 8 (Submissions on Behalf of + Others) for submission on behalf of others). You represent that to + Your knowledge, no other person claims, or has the right to claim, + any right in any intellectual property right related to Your + Contributions. + + You also represent that You are not legally obligated, whether by + entering into an agreement or otherwise, in any way that conflicts + with the terms of this Agreement. + + You represent that Your Contribution submissions include complete + details of any third-party license or other restriction (including, + but not limited to, related patents and trademarks) of which You + are personally aware and which are associated with any part of + Your Contributions. + + 6. You Don't Have an Obligation to Provide Support for Your Contributions + + You are not expected to provide support for Your Contributions, + except to the extent You desire to provide support. You may provide + support for free, for a fee, or not at all. + + 6. No Warranties or Conditions + + StrongLoop acknowledges that unless required by applicable law or + agreed to in writing, You provide Your Contributions on an "AS IS" + BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER + EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES + OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR + FITNESS FOR A PARTICULAR PURPOSE. + + 7. Submission on Behalf of Others + + If You wish to submit work that is not Your original creation, You + may submit it to StrongLoop separately from any Contribution, + identifying the complete details of its source and of any license + or other restriction (including, but not limited to, related + patents, trademarks, and license agreements) of which You are + personally aware, and conspicuously marking the work as + "Submitted on Behalf of a Third-Party: [named here]". + + 8. Agree to Notify of Change of Circumstances + + You agree to notify StrongLoop of any facts or circumstances of + which You become aware that would make these representations + inaccurate in any respect. Email us at callback@strongloop.com. +``` + +[Google C++ Style Guide]: https://google-styleguide.googlecode.com/svn/trunk/cppguide.xml +[Google Javascript Style Guide]: https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..f816ff4 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,7 @@ +strong-build uses a dual license model. + +You may use this library under the terms of the [Artistic 2.0 license][], +or under the terms of the [StrongLoop Subscription Agreement][]. + +[Artistic 2.0 license]: http://opensource.org/licenses/Artistic-2.0 +[StrongLoop Subscription Agreement]: http://strongloop.com/license diff --git a/README.md b/README.md new file mode 100644 index 0000000..c22d576 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# strong-build + +usage: `slb [options]` + +Build a node application package. + +With no options, the default is to install, bundle, and pack. + +When using git, the current branch can be committed onto a deployment +branch before the build. This allows the build products to be +committed and synchronized with a source-only branch. Afterwards, the +deployment branch will be checked-out, ready for build and commit. + +Packages are built without running scripts (by default, see --scripts) +to avoid compiling any binary addons. Compilation and install scripts +should be run on the deployment server using: + + npm rebuild + npm install + +Custom build steps, such as `grunt build`, can be specified as a +'build' script in the package.json's `scripts` property and will +be run after installing dependencies. + +Bundling configures the package.json and .npmignore so deployment (not +development) dependencies as well as any 'build' script output will +not be ignored by `npm pack`. If an .npmignore is created, check it +carefully to ensure build products are packed, but editor and test +ephemera are not. + +Pack output is a tar file in the format produced by `npm pack` and +accepted by `npm install`. + +Committing the build output allows it to be pushed to a PaaS, or +tracked in git. It is committed to current branch (but see `--onto`). + +Options: +* -h,--help Print this message and exit. +* -v,--version Print version and exit. +* -onto BRANCH Merge git HEAD to deployment BRANCH. +* -i,--install Install dependencies (without scripts, by default). +* --scripts If installing, run scripts (to build addons). +* -b,--bundle Modify package to bundle deployment dependencies. +* -p,--pack Pack into a publishable archive (with dependencies). +* -c,--commit Commit build output to current branch. diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 0000000..178905a --- /dev/null +++ b/TODO.txt @@ -0,0 +1,105 @@ +Options: + +X append git build to another branch +X build and package node binaries, or not +- (do later, could be useful for testing) bundle dev dependencies, or not +- `-v,--verbose`, to print all commands and their output to stdout + +https://github.com/davepacheco/node-vasync + +# GIT + +X merge current branch onto a destination branch before building it + +- (later?) TBD support a --from option to merge from other than current branch, or else + a current state that is NOT a branch (like `git co v2.0.2`, a detached head) + +- (later?) TBD Might need to give a commit message or template for the new commit + +X commit build products + + $ git add --force --all . && git commit + +- remove $0 from lib/git.js + +Build: + +X npm install --ignore-scripts (don't ignore scripts if --binary is requested) +- (no, we don't know the target to run) ... run grunt? +- (could run bower, do later, use build script for now) ... run bower? +X ... npm run build (sl specific) ... maybe make people put "grunt" in as + the build script? maybe just run it if its there? note: don't have + to check return of npm run build, it suceeds if no such script... + + https://www.npmjs.org/doc/misc/npm-scripts.html + + can't use prepublish... because it will be run later on deploy server + during `npm install` :-( + +Package: + +X git add --force --all . && git commit + +- TBD git archive HEAD --prefix=package -o out.tar.gz/out.tar (autodetects format) + +Script the archive, but ensure that the tarballs have same format as +npm packed tarballs. + +Don't do, since we aren't building a development-branch: + +- .gitignore update: npm rebuild; git status|grep new >> .gitignore +- git ci --amend .gitignore + +Problem with npm rebuild... loopback oracle :-( It downloads at run-time, +so demonstrates that a sufficiently weird npm package might still require +network access: + +``` +sam@samtu:~/w/sn/sls-sample-app (try-build) % npm rebuild + +> loopback-oracle-installer@1.2.0 install /home/sam/w/sn/sls-sample-app/node_modules/loopback-connector-oracle/node_modules/loopback-oracle-installer +> node pkginstall.js + +Downloading/Extracting http://7e9918db41dd01dbf98e-ec15952f71452bc0809d79c86f5751b6.r22.cf1.rackcdn.com/loopback-oracle-Linux-x64-1.2.0.tar.gz +``` + +# non-git + +Build: + +(as above) + +Package: + +X create an empty .npmignore if one doesn't exist but .gitignore does, + otherwise npm ignores any build products in .gitignore + +node_modules is unconditionally ignored, see +https://www.npmjs.org/doc/developers.html. So, we will have to bundle the deps. + +X npm dependency bundling + +Note an issue with my approach of looking in node_modules... it means that peer +deps of dev dependencies will get packaged... perhaps I should use only the +deps in the package.json (and MAYBE check for existence in node_modules for +optional packages). recall the npm install on build server will attempt to +install any missing optional packages, so having optional packages for a +deployable app that are missing is pretty suspicous, and not likely to do +anything useful. + +X don't read node_modules to find deps to bundle, require presence + in package.json + +X don't bundle deps if they already have a spec for bundled, otherwise, + bundle all non-dev + +X npm pack + +X put tarball one directory up, instead of CWD? + +- TBD test against a repo with a build script, perhaps grunt or bower + +Tried git@github.com:strongloop/loopback-example-full-stack + +Doesn't work for now... I can't get the app to work manually, I need a +walk-through. diff --git a/bin/slb b/bin/slb new file mode 100755 index 0000000..0b2504e --- /dev/null +++ b/bin/slb @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +require('../').build(process.argv, function(er) { + if (!er) { + process.exit(0); + } + process.exit(1); +}); diff --git a/index.js b/index.js new file mode 100644 index 0000000..83c65d5 --- /dev/null +++ b/index.js @@ -0,0 +1,300 @@ +var assert = require('assert'); +var debug = require('debug')('strong-build'); +var fs = require('fs'); +var git = require('./lib/git'); +var json = require('json-file-plus'); +var lodash = require('lodash'); +var Parser = require('posix-getopt').BasicParser; +var path = require('path'); +var shell = require('shelljs'); +var vasync = require('vasync'); + +function printHelp($0, prn) { + prn('usage: %s [options]', $0); + prn(''); + prn('Build a node application package.'); + prn(''); + prn('With no options, the default is to install, bundle, and pack.'); + prn(''); + prn('When using git, the current branch can be committed onto a deployment'); + prn('branch before the build. This allows the build products to be'); + prn('committed and synchronized with a source-only branch. Afterwards, the'); + prn('deployment branch will be checked-out, ready for build and commit.'); + prn(''); + prn('Packages are built without running scripts (by default, see --scripts)'); + prn('to avoid compiling any binary addons. Compilation and install scripts'); + prn('should be run on the deployment server using:'); + prn(''); + prn(' npm rebuild'); + prn(' npm install'); + prn(''); + prn('Custom build steps, such as `grunt build`, can be specified as a'); + prn('\'build\' script in the package.json\'s `scripts` property and will'); + prn('be run after installing dependencies.'); + prn(''); + prn('Bundling configures the package.json and .npmignore so deployment (not'); + prn('development) dependencies as well as any \'build\' script output will'); + prn('not be ignored by `npm pack`. If an .npmignore is created, check it'); + prn('carefully to ensure build products are packed, but editor and test'); + prn('ephemera are not.'); + prn(''); + prn('Pack output is a tar file in the format produced by `npm pack` and'); + prn('accepted by `npm install`.'); + prn(''); + prn('Committing the build output allows it to be pushed to a PaaS, or'); + prn('tracked in git. It is committed to current branch, and best combined'); + prn('with `--onto` to avoid having build products on non-deploy branches.'); + prn(''); + prn('Options:'); + prn(' -h,--help Print this message and exit.'); + prn(' -v,--version Print version and exit.'); + prn(' -onto BRANCH Merge git HEAD to deployment BRANCH.'); + prn(' -i,--install Install dependencies (without scripts, by default).'); + prn(' --scripts If installing, run scripts (to build addons).'); + prn(' -b,--bundle Modify package to bundle deployment dependencies.'); + prn(' -p,--pack Pack into a publishable archive (with dependencies).'); + prn(' -c,--commit Commit build output to current branch.'); +} + +function runCommand(cmd, callback) { + debug('run command: %s', cmd); + shell.exec(cmd, {silent: true}, function(code, output) { + debug('code %d: <<<\n%s>>>', code, output); + if (code !== 0) { + var er = Error(cmd); + } + return callback(er, output, code); + }); +} + +function reportRunError(er, output) { + if (!er) return; + + console.error("Failed to run `%s`:", er.message); + if (output && output !== '') { + process.stderr.write(output); + } +} + +exports.build = function build(argv, callback) { + var $0 = process.env.SLC_COMMAND ? + 'slc ' + process.env.SLC_COMMAND : + path.basename(argv[1]); + var parser = new Parser([ + ':v(version)', + 'h(help)', + 's(scripts)', + 'i(install)', + 'b(bundle)', + 'p(pack)', + 'O:(onto)', + 'c(commit)', + 'N(no-commit)', + ].join(''), + argv); + var option; + var onto; + var install; + var scripts; + var bundle; + var pack; + var commit; + + while ((option = parser.getopt()) !== undefined) { + switch (option.option) { + case 'v': + console.log(require('./package.json').version); + return callback(); + case 'h': + printHelp($0, console.log); + return callback(); + case 's': + scripts = true; + break; + case 'i': + install = true; + break; + case 'b': + bundle = true; + break; + case 'p': + pack = true; + break; + case 'O': + onto = option.optarg; + break; + case 'c': + commit = true; + break; + case 'N': + commit = false; + break; + default: + console.error('Invalid usage (near option \'%s\'), try `%s --help`.', + option.optopt, $0); + return callback(Error('usage')); + } + } + + if (parser.optind() !== argv.length) { + console.error('Invalid usage (extra arguments), try `%s --help`.'); + return callback(Error('usage')); + } + + // With no actions selected, do everything we can (onto requires an argument, + // so we can't do it automatically). + if (!onto && !install && !bundle && !pack && !commit) { + install = bundle = pack = true; + } + + var steps = []; + + if (onto) { + steps.push(doGitOnto); + } + + if (install) { + steps.push(doNpmInstall); + } + + if (bundle) { + steps.push(doBundle); + } + + if (pack) { + steps.push(doNpmPack); + } + + if (commit) { + steps.push(doGitCommit); + } + + vasync.pipeline({funcs: steps}, callback); + + function doGitOnto(_, callback) { + try { + var info = git.onto(onto); + console.log('%s: merged `%s` onto `%s`', + $0, info.srcBranch, info.dstBranch); + return callback(); + } catch(er) { + console.error('%s: %s', $0, er.message); + return callback(er); + } + } + + function doNpmInstall(_, callback) { + var npmInstall = 'npm install'; + if (!scripts) { + npmInstall += ' --ignore-scripts'; + } + runCommand(npmInstall, function(er, output) { + if (er) { + console.error('%s: error during dependency installation', $0); + reportRunError(er, output); + return callback(er); + } + console.log('%s: installed with `%s`', $0, npmInstall); + return doBuildScript(_, callback); + }); + } + + function doBuildScript(_, callback) { + runCommand('npm run build', function(er, output) { + if (er) { + console.error('%s: error in package build script', $0); + reportRunError(er, output); + return callback(er); + } + console.log('%s: ran custom build with `npm run build`', $0); + return callback(); + }); + } + + function doBundle(_, callback) { + // Build output won't get packed if it is .npmignored (a configuration + // error, don't .npmignore your build output) or if there is no .npmignore, + // if it is .gitignored (as they should be). So, create an empty .npmignore + // if there is a .gitignore but not a .npmignore so build products are + // packed. + if (fs.existsSync('.gitignore') && !fs.existsSync('.npmignore')) { + console.warn('%s: creating an empty .npmignore (please check)', $0); + fs.close(fs.openSync('.npmignore', 'a')); + } + + // node_modules is unconditionally ignored by npm pack, the only way to get + // the dependencies packed is to name them in the package.json's + // bundledDepenencies. + var info = require(path.resolve('package.json')); + + var bundled = info.bundleDependencies || info.bundledDependencies; + + debug('found bundled: %j', bundled); + + if (info.bundleDependencies || info.bundledDependencies) { + // Use package specified dependency bundling + return callback(); + } + + // Bundle non-dev dependencies. Optional deps may fail to build at deploy + // time, that's OK, but must be present during packing. If the user has + // more specific desires, they can configure the dependencies themselves, or + // just not run the --bundle action. + bundled = lodash.union( + Object.keys(info.dependencies), + Object.keys(info.optionalDependencies) + ).sort(); + + debug('saving bundled: %j', bundled); + + // Re-write package.json, preserving its format if possible. + json('package.json', function(er, p) { + if (er) { + console.error('%s: error reading package.json: %s', $0, er.message); + return callback(er); + } + + p.data.bundleDependencies = bundled; + + p.save(function(er) { + if (er) { + console.error('%s: error writing package.json: %s', $0, er.message); + return callback(er); + } + console.log('%s: saved bundled dependencies in package.json', $0); + return callback(); + }); + }); + } + + function doNpmPack(_, callback) { + runCommand('npm pack', function(er, output) { + if (er) { + console.error('%s: error packing an archive', $0); + reportRunError(er, output); + return callback(er); + } + + // npm pack output is a single line with the pack file name + var src = output.split('\n')[0]; + var dst = path.join('..', src); + + shell.mv('-f', src, dst); + + console.log('%s: packed into `%s`', $0, dst); + + return callback(); + }); + } + + function doGitCommit(_, callback) { + try { + var info = git.commitAll(); + console.log('%s: committed build products onto `%s`', $0, info.branch); + return callback(); + } catch(er) { + console.error('%s: %s', $0, er.message); + return callback(er); + } + } +}; diff --git a/lib/git.js b/lib/git.js new file mode 100644 index 0000000..4da4ba0 --- /dev/null +++ b/lib/git.js @@ -0,0 +1,103 @@ +var assert = require('assert'); +var debug = require('debug')('strong-build'); +var fs = require('fs'); +var lodash = require('lodash'); +var path = require('path'); +var shell = require('shelljs'); +var util = require('util'); + +function fmt() { + return util.format.apply(util, arguments); +} + +function execSync(cmd, error) { + var exec = shell.exec(cmd, {silent: true}); + + debug('exec `%s`: ...', cmd); + + exec.output = exec.output.trim(); + + debug('exec `%s`: (code %s) %s', cmd, exec.code, exec.output); + + if (exec.code !== 0) { + throw Error(error); + } + + return exec.output; +} + +exports.isGit = function gitIsGit() { + try { + execSync('git symbolic-ref --short HEAD', '(do not care)'); + return true; + } catch(er) { + return false; + } +} + +exports.onto = function gitOnto(gitDstBranch) { + // Get name of current branch, and the tree SHA of its HEAD commit + var gitSrcBranch = execSync( + 'git symbolic-ref --short HEAD', + 'failed to parse branch `HEAD`, is this a git repository?' + ); + + var gitSrcTreeSha = execSync( + 'git log -1 --pretty=format:"%t" HEAD', + 'failed to parse branch `HEAD`, is this a git repository?' + ); + + // Get ref name of destination branch, to validate it exists, and because + // we will use it later. + var dstRefInfo = execSync( + fmt('git show-ref --heads --abbrev "%s"', gitDstBranch), + fmt('failed to parse branch `%s`, does it exist?', gitDstBranch) + ); + var dstRef = dstRefInfo.split(' ')[1]; + + // Commit the source tree to the head of the destination branch, and update + // the destination branch ref. + var message = fmt('Commit branch `%s` onto `%s`', gitSrcBranch, gitDstBranch); + var commitSha = execSync( + fmt('git commit-tree -p HEAD -p "%s" -m \'%s\' %s', + gitDstBranch, message, gitSrcTreeSha), + fmt('failed to commit `%s` onto `%s`', gitSrcBranch, gitDstBranch) + ); + + execSync( + fmt('git update-ref "%s" %s', dstRef, commitSha), + fmt('failed to merge `%s` onto `%s`', gitSrcBranch, gitDstBranch) + ); + + // Checkout the destination branch so that we are ready to build + execSync( + fmt('git checkout "%s"', gitDstBranch), + fmt('failed to checkout `%s`', gitDstBranch) + ); + + return { + dstBranch: gitDstBranch, + srcBranch: gitSrcBranch, + srcTreeSha: gitSrcTreeSha, + commitSha: commitSha, + }; +} + +exports.commitAll = function gitCommitAll() { + var gitSrcBranch = execSync( + 'git symbolic-ref --short HEAD', + 'failed to parse branch `HEAD`, is this a git repository?' + ); + execSync( + fmt('git add --force --all .'), + fmt('failed to add build products to `%s`', gitSrcBranch) + ); + execSync( + fmt('git commit -m "Commit build products"'), + fmt('failed to commit build products to `%s`', gitSrcBranch) + ); + + return { + branch: gitSrcBranch, + }; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9f18e33 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "strong-build", + "version": "0.0.0", + "repository": { + "type": "git", + "url": "git://github.com/strongloop/strong-build.git" + }, + "author": "engineering@strongloop.com", + "license": { + "name": "Dual MIT/StrongLoop", + "url": "https://github.com/strongloop/strong-build/blob/master/LICENSE" + }, + "readmeFilename": "README.md", + "main": "index.js", + "scripts": { + "test": "tap --timeout=300 ./test/test-*.js" + }, + "bin": { + "sl-build": "./bin/slb", + "slb": "./bin/slb" + }, + "dependencies": { + "debug": "^0.8.1", + "shelljs": "^0.3.0", + "posix-getopt": "^1.0.0", + "vasync": "^1.4.3", + "json-file-plus": "^0.2.3", + "lodash": "^2.4.1" + }, + "devDependencies": { + "tap": "^0.4.9", + "async": "^0.9.0", + "tar": "^0.1.19" + } +} diff --git a/test/build-example.js b/test/build-example.js new file mode 100644 index 0000000..726f9c1 --- /dev/null +++ b/test/build-example.js @@ -0,0 +1,66 @@ +module.exports = function buildExample(args, callback) { + assert = require('assert'); + debug = require('debug')('strong-build:test'); + fs = require('fs'); + path = require('path'); + tar = require('tar'); + util = require('util'); + zlib = require('zlib'); + + tar.list = function list(tarfile, callback) { + var paths = []; + + fs.createReadStream(tarfile) + .on('error', callback) + .pipe(zlib.Unzip()) + .pipe(tar.Parse()) + .on('entry', function(entry) { + paths.push(entry.path); + }) + .on('end', function() { + debug('tarfile %s list:', tarfile, paths); + return callback(null, paths); + }); + }; + + fs.readJsonSync = function readJsonSync(file) { + return JSON.parse(fs.readFileSync(file)); + }; + + require('shelljs/global'); + + var build = require('../'); + + // Check for node silently exiting with code 0 when tests have not passed. + var ok = false; + + process.on('exit', function(code) { + if (code === 0) { + assert(ok); + } + }); + + + console.log('build example with %j in %s with %s at %s', + args, + pwd(), + which('slc'), + exec('slc -v', {silent: true}).output.split('\n')[0].split(' ')[1] + ); + + rm('-rf', '_suite'); + exec('slc example suite _suite --no-install', {silent: true}); + cd('_suite'); + assert(!test('-d', 'node_modules')); + + var argv = ['node', 'slb'].concat(args); + + debug('build with argv:', argv); + + build.build(argv, function(er) { + ok = true; + return callback(er) + }); + + debug('waiting for build...'); +}; diff --git a/test/test-build-bundled.js b/test/test-build-bundled.js new file mode 100644 index 0000000..a0f0eed --- /dev/null +++ b/test/test-build-bundled.js @@ -0,0 +1,36 @@ +var lodash = require('lodash'); +require('./build-example')(['--install', '-p', '--bundle'], function(er) { + assert.ifError(er); + var info = fs.readJsonSync('package.json'); + var tgz = path.join('..', util.format('%s-%s.tgz', info.name, info.version)); + + // loopback should be bundled (normal dep) + // loopback-connector-mysql should be bundled (optional dep) + // mocha should not (dev dep) + function isBundled(bundled, name) { + return lodash.findIndex(bundled, function(_) { + return _ === name; + }) >= 0; + } + debug('package bundles:', info.bundleDependencies); + assert(isBundled(info.bundleDependencies, 'loopback')); + assert(isBundled(info.bundleDependencies, 'loopback-connector-mysql')); + assert(!isBundled(info.bundleDependencies, 'mocha')); + + tar.list(tgz, function(er, paths) { + debug('tarfile %s bundles:', tgz, bundled); + var bundled = lodash(paths).map(function(_) { + // We are looking for the top-level bundled deps, they look like + // package/node_modules/shuffle/package.json + var components = _.split(path.sep); + + if (components.length === 4 && components[3] === 'package.json') { + return components[2]; + } + return undefined; + }).compact().sort().value(); + debug('tarfile %s bundles:', tgz, bundled); + debug('should be:', info.bundleDependencies); + assert.deepEqual(bundled, info.bundleDependencies); + }); +}); diff --git a/test/test-build-install.js b/test/test-build-install.js new file mode 100644 index 0000000..8085d91 --- /dev/null +++ b/test/test-build-install.js @@ -0,0 +1,16 @@ +require('./build-example')(['-i'], function(er) { + debug('built with error?', er); + assert.ifError(er); + assert(test('-d', 'node_modules')); + + var addons = find('node_modules').filter(function(path) { + // .../mongodb/node_modules/bson/ contains compiled addons. Others could in + // the future, but for now, this works to assert addons weren't compiled + // during the `npm install` + return path.match(/.*\.node$/) && !path.match(/node_modules\/bson/); + }); + + debug('addons:', addons); + + assert.equal(addons.length, 0); +}); diff --git a/test/test-build-pack.js b/test/test-build-pack.js new file mode 100644 index 0000000..2a43aa1 --- /dev/null +++ b/test/test-build-pack.js @@ -0,0 +1,18 @@ +require('./build-example')(['--install', '-p'], function(er) { + debug('built with error?', er); + assert.ifError(er); + assert(test('-d', 'node_modules')); + var info = require(path.resolve('package.json')); + assert.equal(info.name, 'sls-sample-app'); + var tgz = path.join('..', util.format('%s-%s.tgz', info.name, info.version)); + assert(test('-f', tgz), 'expected to find ' + tgz); + + tar.list(tgz, function(er, paths) { + var bundled = paths.filter(function(path) { + return path.match(/node_modules/); + }); + debug('tarfile %s bundles:', tgz, bundled); + + assert.equal(bundled.length, 0, 'bundling not requested'); + }); +}); diff --git a/test/test-build-scripts.js b/test/test-build-scripts.js new file mode 100644 index 0000000..7a9afd5 --- /dev/null +++ b/test/test-build-scripts.js @@ -0,0 +1,16 @@ +require('./build-example')(['-i', '--scripts'], function(er) { + debug('built with error?', er); + assert.ifError(er); + assert(test('-d', 'node_modules')); + + var addons = find('node_modules').filter(function(path) { + // .../mongodb/node_modules/bson/ contains compiled addons. Others could in + // the future, but for now, this works to assert addons weren't compiled + // during the `npm install` + return path.match(/.*\.node$/) && !path.match(/node_modules\/bson/); + }); + + debug('addons:', addons); + + assert(addons.length > 0); +}); diff --git a/test/test-onto.js b/test/test-onto.js new file mode 100644 index 0000000..70a458e --- /dev/null +++ b/test/test-onto.js @@ -0,0 +1,94 @@ +var assert = require('assert'); +var debug = require('debug')('strong-build:test'); +var fs = require('fs'); +var path = require('path'); +var tar = require('tar'); +var util = require('util'); +var vasync = require('vasync'); +var zlib = require('zlib'); + +var shell = require('shelljs/global'); + +// Check for node silently exiting with code 0 when tests have not passed. +var ok = false; + +process.on('exit', function(code) { + if (code === 0) { + assert(ok); + } +}); + +console.log('pwd: %s', pwd()); + +// Create two branches that are different, and commit the src +// over the dst, and then diff them to prove identical. +rm('-rf', '_onto'); +mkdir('_onto'); +cd('_onto'); +run('git init'); +touchAndCommit('.gitignore'); +run('git checkout -b dst master'); +touchAndCommit('only-on-dst'); +run('git checkout -b src master'); +touchAndCommit('only-on-src'); + +assert(!branchEqual('src', 'dst')); + +var build = require('../'); + +function onto(_, callback) { + build.build(['node', 'main.js', '--onto=dst'], function(er) { + if (er) return callback(er); + assert(branchEqual('src', 'dst')); + return callback(); + }); +} + +// simulate build, by generating output +touch('build.out'); + +function commit(_, callback) { + build.build(['node', 'main.js', '-c'], function(er) { + if (er) return callback(er); + run('git log build.out'); // check build.out was committed + return callback(); + }); +} + +vasync.pipeline({funcs: [onto, commit]}, function(er) { + assert.ifError(er); + ok = true; +}); + +// Shell wrappers +function run() { + var cmd = util.format.apply(util, arguments); + debug('exec `%s`: ...', cmd); + var out = exec(cmd, {silent: true}); + out.output = out.output.trim(); + console.log('exec `%s`: (code %s)\n%s', cmd, out.code, out.output); + if (out.code !== 0) { + throw Error(error); + } + return out.output; +} + +function touch(name) { + console.log('touch %s', name); + fs.closeSync(fs.openSync(name, 'a')); +} + +function touchAndCommit(name) { + touch(name); + run('git add %s', name); + run('git commit -m add-%s', name); +} + +function branchEqual(src, dst) { + try { + run('git diff --quiet %s %s', src, dst); + return true; + } catch(er) { + return false; + } +} diff --git a/test/test-usage.js b/test/test-usage.js new file mode 100644 index 0000000..2db151f --- /dev/null +++ b/test/test-usage.js @@ -0,0 +1,54 @@ +var assert = require('assert'); +var async = require('async'); +var debug = require('debug')('strong-build:test'); +var path = require('path'); + +require('shelljs/global'); + +var build = require('../').build; + +// Check for node silently exiting with code 0 when tests have not passed. +var ok = false; + +process.on('exit', function(code) { + if (code === 0) { + assert(ok); + } +}); + +function expectError(er) { + if(er) { + return null; + } else { + return Error('expected error'); + } +} + +// argv [0] and [1] are ignored (they are node and script name, not options) +async.parallel([ + build.bind(null, ['', '', '-h']), + build.bind(null, ['', '', '--help']), + build.bind(null, ['', '', '-hv']), + build.bind(null, ['', '', '-v']), + build.bind(null, ['', '', '--version']), + build.bind(null, ['', '', '-vh']), + function(callback) { + build(['', '', 'no-such-arg'], function(er) { + return callback(expectError(er)); + }); + }, + function(callback) { + build(['', '', '--no-such-option'], function(er) { + return callback(expectError(er)); + }); + }, + function(callback) { + build(['', '', '-Z'], function(er) { + return callback(expectError(er)); + }); + }, +], function(er, results) { + debug('test-help: error=%s:', er, results); + assert.ifError(er); + ok = true; +});