From 44b0b2a64dc836a7d633f364e388dcda848ddc6a Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Tue, 3 Jul 2018 04:40:15 -0700 Subject: [PATCH] Java init template (#229) The template includes a java shim for the toolkit called "app.sh". The shim depends on a file `.classpath.txt` that's generated by maven during build. It points to `target/classes` for classpath. This supports a workflow where you can iterate in the IDE (and run unit tests), but then go to the command line to execute the toolkit, and you will target the same classes the IDE compiled. BETA: the template includes a pom.xml file that depends on `~/.cdk/repo/maven` as a pragmatic solution for beta. There's value in adding the repository in the pom.xml file directly as this gets us seamless IDE experience that works out of the box. This change also modifies the pom.xml file for aws-cdk-java to not explicitly include the local jsii repository, which is temporary anyway until we publish to Maven Central. This allows us to include the full pom.xml in the maven package, so transitive dependencies work. --- packages/@aws-cdk/cloudwatch/.gitignore | 1 + packages/@aws-cdk/codepipeline/.gitignore | 1 + packages/aws-cdk-java/generate.sh | 5 + packages/aws-cdk-java/package.json | 4 +- packages/aws-cdk-java/pom.xml.t.js | 15 +- .../lib/init-templates/app/java/.gitignore | 8 + .../lib/init-templates/app/java/README.md | 24 ++ .../lib/init-templates/app/java/app.sh | 6 + .../lib/init-templates/app/java/cdk.json | 3 + .../init-templates/app/java/pom.template.xml | 69 +++++ .../src/main/java/com/myorg/HelloApp.java | 16 ++ .../main/java/com/myorg/HelloConstruct.java | 31 +++ .../java/com/myorg/HelloConstructProps.java | 40 +++ .../src/main/java/com/myorg/HelloStack.java | 37 +++ .../test/java/com/myorg/HelloStackTest.java | 28 ++ .../resources/com/myorg/expected.cfn.json | 255 ++++++++++++++++++ packages/aws-cdk/lib/init.ts | 59 ++-- 17 files changed, 567 insertions(+), 35 deletions(-) create mode 100644 packages/aws-cdk/lib/init-templates/app/java/.gitignore create mode 100644 packages/aws-cdk/lib/init-templates/app/java/README.md create mode 100755 packages/aws-cdk/lib/init-templates/app/java/app.sh create mode 100644 packages/aws-cdk/lib/init-templates/app/java/cdk.json create mode 100644 packages/aws-cdk/lib/init-templates/app/java/pom.template.xml create mode 100644 packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloApp.java create mode 100644 packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloConstruct.java create mode 100644 packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloConstructProps.java create mode 100644 packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloStack.java create mode 100644 packages/aws-cdk/lib/init-templates/app/java/src/test/java/com/myorg/HelloStackTest.java create mode 100644 packages/aws-cdk/lib/init-templates/app/java/src/test/resources/com/myorg/expected.cfn.json diff --git a/packages/@aws-cdk/cloudwatch/.gitignore b/packages/@aws-cdk/cloudwatch/.gitignore index 541a7635fd070..9178900e205b8 100644 --- a/packages/@aws-cdk/cloudwatch/.gitignore +++ b/packages/@aws-cdk/cloudwatch/.gitignore @@ -5,3 +5,4 @@ tslint.json *.d.ts dist lib/generated/resources.ts +*.tgz diff --git a/packages/@aws-cdk/codepipeline/.gitignore b/packages/@aws-cdk/codepipeline/.gitignore index 541a7635fd070..9178900e205b8 100644 --- a/packages/@aws-cdk/codepipeline/.gitignore +++ b/packages/@aws-cdk/codepipeline/.gitignore @@ -5,3 +5,4 @@ tslint.json *.d.ts dist lib/generated/resources.ts +*.tgz diff --git a/packages/aws-cdk-java/generate.sh b/packages/aws-cdk-java/generate.sh index bd9481fc9cfc1..6b84fa6d694d7 100755 --- a/packages/aws-cdk-java/generate.sh +++ b/packages/aws-cdk-java/generate.sh @@ -3,6 +3,11 @@ set -euo pipefail mkdir -p project +# deploy jsii-runtime to the local maven repo so it will be discoverable +jsii_runtime_repo=$(node -e "console.log(path.join(path.dirname(require.resolve('jsii-java-runtime/package.json')), 'maven-repo'))") +mkdir -p ~/.m2/repository +rsync -av ${jsii_runtime_repo}/ ~/.m2/repository/ + echo "Generating pom.xml..." node ./pom.xml.t.js > project/pom.xml diff --git a/packages/aws-cdk-java/package.json b/packages/aws-cdk-java/package.json index 40184c4b4c6e3..132f9814ccd02 100644 --- a/packages/aws-cdk-java/package.json +++ b/packages/aws-cdk-java/package.json @@ -20,10 +20,8 @@ "url": "https://aws.amazon.com" }, "license": "Apache-2.0", - "dependencies": { - "aws-cdk-all": "^0.7.2-beta" - }, "devDependencies": { + "aws-cdk-all": "^0.7.2-beta", "pkgtools": "^0.7.2-beta" }, "keywords": [ diff --git a/packages/aws-cdk-java/pom.xml.t.js b/packages/aws-cdk-java/pom.xml.t.js index f4b218bc06763..e7ce6ce8e3374 100644 --- a/packages/aws-cdk-java/pom.xml.t.js +++ b/packages/aws-cdk-java/pom.xml.t.js @@ -1,10 +1,7 @@ const path = require('path'); const version = require('./package.json').version.replace(/\+.+$/, ''); // omit "+build" postfix -const jsiiRuntime = { - version: require('jsii-java-runtime/package.json').version, - repo: path.join(path.dirname(require.resolve('jsii-java-runtime')), 'maven-repo'), -}; +const jsiiRuntimeVersion = require('jsii-java-runtime/package.json').version; process.stdout.write(` UTF-8 - - - jsii - file://${jsiiRuntime.repo} - - - @@ -75,6 +65,7 @@ process.stdout.write(` \${project.groupId} \${project.artifactId} \${project.version} + \${project.basedir}/pom.xml jar @@ -88,7 +79,7 @@ process.stdout.write(` com.amazonaws jsii-runtime - ${jsiiRuntime.version} + ${jsiiRuntimeVersion} diff --git a/packages/aws-cdk/lib/init-templates/app/java/.gitignore b/packages/aws-cdk/lib/init-templates/app/java/.gitignore new file mode 100644 index 0000000000000..977763f5766de --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/.gitignore @@ -0,0 +1,8 @@ +.classpath.txt +target +.classpath +.project +.idea +.settings +.vscode +*.iml diff --git a/packages/aws-cdk/lib/init-templates/app/java/README.md b/packages/aws-cdk/lib/init-templates/app/java/README.md new file mode 100644 index 0000000000000..b74767ee1b0f8 --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/README.md @@ -0,0 +1,24 @@ + +Welcome to your CDK Java project! + +It is a Maven-based project, so you can open this directory with any Maven-compatible Java IDE, +and you should be able to build and run tests from your IDE. + +You should explore the contents of this template. It demonstrates a CDK app with two instances of +a stack (`HelloStack`) which also uses a user-defined construct (`HelloConstruct`). + +The `cdk.json` file tells the CDK Toolkit how to execute your app. It uses a script called `app.sh` +to do that. Note that this script expects a local file called `.classpath.txt` to exist. This file +is automatically created by `mvn package`. + +# Useful commands + + * `mvn package` compile and run tests + * `cdk ls` list all stacks in the app + * `cdk synth` emits the synthesized CloudFormation template + * `cdk deploy` deploy this stack to your default AWS account/region + * `cdk diff` compare deployed stack with current state + * `cdk docs` open CDK documentation + +Enjoy! + diff --git a/packages/aws-cdk/lib/init-templates/app/java/app.sh b/packages/aws-cdk/lib/init-templates/app/java/app.sh new file mode 100755 index 0000000000000..210e90b76344a --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/app.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# This script is configured in cdk.json to be used to execute +# the CDK java app by the command-line toolkit. +# The file .classpath.txt is created by when `mvn package` is called +# The first argument will be used as argv[0] +exec java -cp target/classes:$(cat .classpath.txt) com.myorg.HelloApp hello-cdk $@ diff --git a/packages/aws-cdk/lib/init-templates/app/java/cdk.json b/packages/aws-cdk/lib/init-templates/app/java/cdk.json new file mode 100644 index 0000000000000..676ae2b6bd071 --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "./app.sh" +} diff --git a/packages/aws-cdk/lib/init-templates/app/java/pom.template.xml b/packages/aws-cdk/lib/init-templates/app/java/pom.template.xml new file mode 100644 index 0000000000000..2e60478c6f621 --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/pom.template.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + + + cdk + file://%cdk-home%/repo/maven + + + + com.myorg + %name% + 0.1 + + + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.7.0 + + 1.8 + 1.8 + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.8 + + + build-classpath + generate-sources + + build-classpath + + + .classpath.txt + + + + + + + + + + + com.amazonaws.cdk + aws-cdk + LATEST + + + + + junit + junit + 4.12 + test + + + diff --git a/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloApp.java b/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloApp.java new file mode 100644 index 0000000000000..d0debb8f5ed38 --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloApp.java @@ -0,0 +1,16 @@ +package com.myorg; + +import com.amazonaws.cdk.App; + +import java.util.Arrays; + +public class HelloApp { + public static void main(final String argv[]) { + App app = new App(Arrays.asList(argv)); + + new HelloStack(app, "hello-cdk-1"); + new HelloStack(app, "hello-cdk-2"); + + System.out.println(app.run()); + } +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloConstruct.java b/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloConstruct.java new file mode 100644 index 0000000000000..aaf2bb40550f9 --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloConstruct.java @@ -0,0 +1,31 @@ +package com.myorg; + +import com.amazonaws.cdk.Construct; +import com.amazonaws.cdk.iam.IIdentityResource; +import com.amazonaws.cdk.s3.Bucket; + +import java.util.ArrayList; +import java.util.List; + +/** + * Example of a reusable construct. This one defines N buckets. + */ +public class HelloConstruct extends Construct { + private final List buckets = new ArrayList<>(); + + public HelloConstruct(final Construct parent, final String name, final HelloConstructProps props) { + super(parent, name); + + for (int i = 0; i < props.getBucketCount(); ++i) { + buckets.add(new Bucket(this, "Bucket" + String.valueOf(i))); + } + } + + /** + * Given an principal, grants it READ access on all buckets. + * @param principal The principal (User, Group, Role) + */ + public void grantRead(final IIdentityResource principal) { + buckets.forEach(b -> b.grantRead(principal)); + } +} diff --git a/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloConstructProps.java b/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloConstructProps.java new file mode 100644 index 0000000000000..592a64e45b173 --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloConstructProps.java @@ -0,0 +1,40 @@ +package com.myorg; + +public class HelloConstructProps { + private int bucketCount; + + public static HelloConstructPropsBuilder builder() { + return new HelloConstructPropsBuilder(); + } + + public int getBucketCount() { + return bucketCount; + } + + public void setBucketCount(int bucketCount) { + this.bucketCount = bucketCount; + } + + + public static final class HelloConstructPropsBuilder { + private int bucketCount; + + private HelloConstructPropsBuilder() { + } + + public static HelloConstructPropsBuilder aHelloConstructProps() { + return new HelloConstructPropsBuilder(); + } + + public HelloConstructPropsBuilder withBucketCount(int bucketCount) { + this.bucketCount = bucketCount; + return this; + } + + public HelloConstructProps build() { + HelloConstructProps helloConstructProps = new HelloConstructProps(); + helloConstructProps.setBucketCount(bucketCount); + return helloConstructProps; + } + } +} diff --git a/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloStack.java b/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloStack.java new file mode 100644 index 0000000000000..4a55d78209a8b --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloStack.java @@ -0,0 +1,37 @@ +package com.myorg; + +import com.amazonaws.cdk.App; +import com.amazonaws.cdk.Stack; +import com.amazonaws.cdk.StackProps; +import com.amazonaws.cdk.iam.User; +import com.amazonaws.cdk.sns.Topic; +import com.amazonaws.cdk.sns.TopicProps; +import com.amazonaws.cdk.sqs.Queue; +import com.amazonaws.cdk.sqs.QueueProps; + +public class HelloStack extends Stack { + public HelloStack(final App parent, final String name) { + this(parent, name, null); + } + + public HelloStack(final App parent, final String name, final StackProps props) { + super(parent, name, props); + + Queue queue = new Queue(this, "MyFirstQueue", QueueProps.builder() + .withVisibilityTimeoutSec(300) + .build()); + + Topic topic = new Topic(this, "MyFirstTopic", TopicProps.builder() + .withDisplayName("My First Topic Yeah") + .build()); + + topic.subscribeQueue(queue); + + HelloConstruct hello = new HelloConstruct(this, "Buckets", HelloConstructProps.builder() + .withBucketCount(5) + .build()); + + User user = new User(this, "MyUser"); + hello.grantRead(user); + } +} diff --git a/packages/aws-cdk/lib/init-templates/app/java/src/test/java/com/myorg/HelloStackTest.java b/packages/aws-cdk/lib/init-templates/app/java/src/test/java/com/myorg/HelloStackTest.java new file mode 100644 index 0000000000000..b998dd795af0a --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/src/test/java/com/myorg/HelloStackTest.java @@ -0,0 +1,28 @@ +package com.myorg; + +import com.amazonaws.cdk.App; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.junit.Test; + +import java.io.IOException; + +import static junit.framework.TestCase.assertEquals; + +public class HelloStackTest { + private final static ObjectMapper JSON = + new ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true); + + @Test + public void testStack() throws IOException { + App app = new App(); + HelloStack stack = new HelloStack(app, "test"); + + // synthesize the stack to a CloudFormation template and compare against + // a checked-in JSON file. + JsonNode actual = JSON.valueToTree(app.synthesizeStack(stack.getName()).getTemplate()); + JsonNode expected = JSON.readTree(getClass().getResource("expected.cfn.json")); + assertEquals(expected, actual); + } +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/init-templates/app/java/src/test/resources/com/myorg/expected.cfn.json b/packages/aws-cdk/lib/init-templates/app/java/src/test/resources/com/myorg/expected.cfn.json new file mode 100644 index 0000000000000..97c1ad4d15727 --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/src/test/resources/com/myorg/expected.cfn.json @@ -0,0 +1,255 @@ +{ + "Resources": { + "MyFirstTopic0ED1F8A4": { + "Type": "AWS::SNS::Topic", + "Properties": { + "DisplayName": "My First Topic Yeah" + } + }, + "MyFirstQueuePolicy596EEC78": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Ref": "MyFirstTopic0ED1F8A4" + } + } + }, + "Action": "sqs:SendMessage", + "Resource": { + "Fn::GetAtt": [ + "MyFirstQueueFF09316A", + "Arn" + ] + }, + "Effect": "Allow", + "Principal": { + "Service": "sns.amazonaws.com" + } + } + ] + }, + "Queues": [ + { + "Ref": "MyFirstQueueFF09316A" + } + ] + } + }, + "BucketsBucket1B98A5B53": { + "Type": "AWS::S3::Bucket" + }, + "MyFirstQueueFF09316A": { + "Type": "AWS::SQS::Queue", + "Properties": { + "VisibilityTimeout": 300 + } + }, + "MyFirstTopicMyFirstQueueSubscription04CBE05C": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Endpoint": { + "Fn::GetAtt": [ + "MyFirstQueueFF09316A", + "Arn" + ] + }, + "TopicArn": { + "Ref": "MyFirstTopic0ED1F8A4" + }, + "Protocol": "sqs" + } + }, + "BucketsBucket351AD8978": { + "Type": "AWS::S3::Bucket" + }, + "BucketsBucket237F4E9C5": { + "Type": "AWS::S3::Bucket" + }, + "BucketsBucket4AEE1AAD9": { + "Type": "AWS::S3::Bucket" + }, + "MyUserDC45028B": { + "Type": "AWS::IAM::User" + }, + "MyUserDefaultPolicy7B897426": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyName": "MyUserDefaultPolicy7B897426", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Resource": [ + { + "Fn::GetAtt": [ + "BucketsBucket04F23AD6D", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "BucketsBucket04F23AD6D", + "Arn" + ] + }, + "/", + "*" + ] + ] + } + ], + "Effect": "Allow" + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Resource": [ + { + "Fn::GetAtt": [ + "BucketsBucket1B98A5B53", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "BucketsBucket1B98A5B53", + "Arn" + ] + }, + "/", + "*" + ] + ] + } + ], + "Effect": "Allow" + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Resource": [ + { + "Fn::GetAtt": [ + "BucketsBucket237F4E9C5", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "BucketsBucket237F4E9C5", + "Arn" + ] + }, + "/", + "*" + ] + ] + } + ], + "Effect": "Allow" + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Resource": [ + { + "Fn::GetAtt": [ + "BucketsBucket351AD8978", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "BucketsBucket351AD8978", + "Arn" + ] + }, + "/", + "*" + ] + ] + } + ], + "Effect": "Allow" + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Resource": [ + { + "Fn::GetAtt": [ + "BucketsBucket4AEE1AAD9", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "BucketsBucket4AEE1AAD9", + "Arn" + ] + }, + "/", + "*" + ] + ] + } + ], + "Effect": "Allow" + } + ] + }, + "Users": [ + { + "Ref": "MyUserDC45028B" + } + ] + } + }, + "BucketsBucket04F23AD6D": { + "Type": "AWS::S3::Bucket" + } + } +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/init.ts b/packages/aws-cdk/lib/init.ts index 1330819266b86..b7ba863a02882 100644 --- a/packages/aws-cdk/lib/init.ts +++ b/packages/aws-cdk/lib/init.ts @@ -11,6 +11,7 @@ const decamelize = require('decamelize'); // tslint:enable:no-var-requires const TEMPLATES_DIR = path.join(__dirname, 'init-templates'); +const CDK_HOME = process.env.CDK_HOME ? path.resolve(process.env.CDK_HOME) : path.join(os.homedir(), '.cdk'); /** * Initialize a CDK package in the current directory @@ -102,11 +103,13 @@ export class InitTemplate { } private expand(template: string, project: ProjectInfo) { - const cdkVersion = require('@aws-cdk/core/package.json').version.replace(/\+[a-f0-9]+$/, ''); + const MATCH_VER_BUILD = /\+[a-f0-9]+$/; // Matches "+BUILD" in "x.y.z-beta+BUILD" + const cdkVersion = require('@aws-cdk/core/package.json').version.replace(MATCH_VER_BUILD, ''); return template.replace(/%name%/g, project.name) .replace(/%name\.camelCased%/g, camelCase(project.name)) .replace(/%name\.PascalCased%/g, camelCase(project.name, { pascalCase: true })) - .replace(/%cdk-version%/g, cdkVersion); + .replace(/%cdk-version%/g, cdkVersion) + .replace(/%cdk-home%/g, CDK_HOME); } } @@ -192,19 +195,27 @@ async function postInstall(language: string) { switch (language) { case 'typescript': return await postInstallTypescript(); + case 'java': + return await postInstallJava(); } } async function postInstallTypescript() { - const cdkHome = process.env.CDK_HOME ? path.resolve(process.env.CDK_HOME) : path.join(os.homedir(), '.cdk'); - const yNpm = path.join(cdkHome, 'bin', 'y-npm'); + const yNpm = path.join(CDK_HOME, 'bin', 'y-npm'); const command = await fs.pathExists(yNpm) ? yNpm : 'npm'; print(`Executing ${colors.green(`${command} install`)}...`); - if (await execute(command, 'install') !== 0) { - throw new Error(`${colors.green(`${command} install`)} failed!`); + try { + await execute(command, 'install'); + } catch (e) { + throw new Error(`${colors.green(`${command} install`)} failed: ` + e.message); } } +async function postInstallJava() { + print(`Executing ${colors.green('mvn package')}...`); + await execute('mvn', 'package'); +} + /** * @param dir a directory to be checked * @returns true if ``dir`` is within a git repository. @@ -225,19 +236,27 @@ function isRoot(dir: string) { return path.dirname(dir) === dir; } -function execute(cmd: string, ...args: string[]): Promise { - return new Promise((resolve, reject) => { - try { - const child = spawn(cmd, args, { stdio: 'inherit' }); - child.on('exit', (code, _signal) => { - if (code != null) { - resolve(code); - } else { - resolve(128); - } - }); - } catch (e) { - reject(e); - } +/** + * Executes `command`. STDERR is emitted in real-time. + * + * If command exits with non-zero exit code, an exceprion is thrown and includes + * the contents of STDOUT. + * + * @returns STDOUT (if successful). + */ +async function execute(cmd: string, ...args: string[]) { + const child = spawn(cmd, args, { stdio: [ 'ignore', 'pipe', 'inherit' ] }); + let stdout = ''; + child.stdout.on('data', chunk => stdout += chunk.toString()); + return new Promise((ok, fail) => { + child.once('error', err => fail(err)); + child.once('exit', status => { + if (status === 0) { + return ok(stdout); + } else { + process.stderr.write(stdout); + return fail(new Error(`${cmd} exited with status ${status}`)); + } + }); }); }