Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds a bubblewrap install command #161

Merged
merged 4 commits into from
May 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,16 @@ Usage:
bubblewrap validate --url=[pwa-url]
```

## `install`

Install the application generated in the output command to a device connected for debugging.

Usage:

```
bubblewrap install [--apkFile="/path-to-apk/apkfile.apk"]
```

## `help`

Displays a list of commands and options.
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/lib/Cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {help} from './cmds/help';
import {build} from './cmds/build';
import {init} from './cmds/init';
import {validate} from './cmds/validate';
import {install} from './cmds/install';
import {loadOrCreateConfig} from './config';
import {major} from 'semver';

Expand All @@ -44,6 +45,8 @@ export class Cli {
return await build(config, parsedArgs);
case 'validate':
return await validate(parsedArgs);
case 'install':
return await install(parsedArgs, config);
default:
throw new Error(`"${command}" is not a valid command!`);
}
Expand Down
13 changes: 13 additions & 0 deletions packages/cli/src/lib/cmds/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const HELP_MESSAGES = new Map<string, string>(
'update .............. updates an existing TWA Project with the latest bubblewrap template',
'validate ............ validates if an URL matches the PWA Quality Criteria for Trusted' +
' Web Activity',
'install ............. installs the output application to a connected device',
].join('\n')],
['init', [
'Usage:',
Expand Down Expand Up @@ -70,6 +71,18 @@ const HELP_MESSAGES = new Map<string, string>(
'',
'bubblewrap validate --url=[pwa-url]',
].join('\n')],
['install', [
'Usage:',
'',
'',
'bubblewrap install',
'',
'',
'Options: ',
'--apkFile ................. path to the APK file to be isntalled. Defaults to ' +
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about adding some option (maybe like debug or verbose) that prints out the adb command we're executing?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

'"./app-release-signed.apk"',
'--verbose ................. prints the adb command being executed',
].join('\n')],
],
);

Expand Down
40 changes: 40 additions & 0 deletions packages/cli/src/lib/cmds/install.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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 {AndroidSdkTools, Config, JdkHelper, Log} from '@bubblewrap/core';
import {ParsedArgs} from 'minimist';

const APK_FILE_PARAM = '--apkFile';
const VERBOSE_PARAM = '--verbose';
const DEFAULT_APK_FILE = './app-release-signed.apk';

export async function install(
args: ParsedArgs, config: Config, log = new Log('install')): Promise<boolean> {
const jdkHelper = new JdkHelper(process, config);
const androidSdkTools = new AndroidSdkTools(process, config, jdkHelper, log);
const apkFile = args.apkFile || DEFAULT_APK_FILE;
if (args.verbose) {
log.verbose = true;
}

// parameter 0 would be the path to 'node', followed by `bubblewrap.js` at 1, then `install` at
// 2. So, we want to start collecting args from parameter 3 and ignore any a possible
// `--apkFile`, which is specific to install. Extra parameters are passed through to `adb`.
const originalArgs = process.argv.slice(3).filter(
(v) => !v.startsWith(APK_FILE_PARAM) && !v.startsWith(VERBOSE_PARAM));
await androidSdkTools.install(apkFile, originalArgs);
return true;
}
18 changes: 18 additions & 0 deletions packages/core/src/lib/androidSdk/AndroidSdkTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,22 @@ export class AndroidSdkTools {
];
await execPromise(apksignerCmd.join(' '), {env: env});
}

/**
* Installs an APK on an a device connected to the computer.
* @param apkFilePath the path to the APK to be installed
*/
async install(apkFilePath: string, passthroughArgs: string[] = []): Promise<void> {
if (!fs.existsSync(apkFilePath)) {
throw new Error(`Could not find APK file at ${apkFilePath}`);
}
const env = this.getEnv();
const installCmd = [
`"${this.pathJoin(this.getAndroidHome(), '/platform-tools/adb')}"`,
...passthroughArgs,
'install',
apkFilePath,
];
await util.execute(installCmd, env, this.log);
}
}
10 changes: 8 additions & 2 deletions packages/core/src/lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {promisify} from 'util';
import {exec, spawn} from 'child_process';
import {x as extractTar} from 'tar';
import {WebManifestIcon} from './types/WebManifest';
import Log from './Log';

const execPromise = promisify(exec);
const extractZipPromise = promisify(extractZip);
Expand All @@ -30,8 +31,13 @@ const extractZipPromise = promisify(extractZip);
const DISALLOWED_ANDROID_PACKAGE_CHARS_REGEX = /[^a-zA-Z0-9_\.]/g;
const VALID_PACKAGE_ID_SEGMENT_REGEX = /^[a-zA-Z][A-Za-z0-9_]*$/;

export async function execute(cmd: string[], env: NodeJS.ProcessEnv): Promise<void> {
await execPromise(cmd.join(' '), {env: env});
export async function execute(
cmd: string[], env: NodeJS.ProcessEnv, log?: Log): Promise<void> {
const joinedCmd = cmd.join(' ');
if (log) {
log.debug(`Executing command: ${joinedCmd}`);
}
await execPromise(joinedCmd, {env: env});
}

export async function downloadFile(url: string, path: string): Promise<void> {
Expand Down
50 changes: 50 additions & 0 deletions packages/core/src/spec/lib/androidSdk/AndroidSdkToolsSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {JdkHelper} from '../../../lib/jdk/JdkHelper';
import {AndroidSdkTools} from '../../../lib/androidSdk/AndroidSdkTools';
import util = require('../../../lib/util');
import * as fs from 'fs';
import {Log} from '../../..';

function buildMockConfig(platform: string): Config {
if (platform === 'linux' || platform == 'darwin') {
Expand Down Expand Up @@ -146,4 +147,53 @@ describe('AndroidSdkTools', () => {
expectAsync(androidSdkTools.installBuildTools()).toBeRejectedWithError();
});
});

describe('#install', () => {
const tests = [
{platform: 'linux',
expectedCwd: [
'"/home/user/android-sdk/platform-tools/adb"',
'install',
'app-release-signed.apk',
]},
{platform: 'darwin',
expectedCwd: [
'"/home/user/android-sdk/platform-tools/adb"',
'install',
'app-release-signed.apk',
]},
{platform: 'win32',
expectedCwd: [
'"C:\\Users\\user\\android-sdk\\platform-tools\\adb"',
'install',
'app-release-signed.apk',
]},
];

tests.forEach((test) => {
it(`Build the correct install command on ${test.platform}`, async () => {
spyOn(fs, 'existsSync').and.returnValue(true);
const config = buildMockConfig(test.platform);
const process = buildMockProcess(test.platform);
const jdkHelper = new JdkHelper(process, config);
const log = new Log('test');
const androidSdkTools = new AndroidSdkTools(process, config, jdkHelper, log);
spyOn(util, 'execute').and.stub();
await androidSdkTools.install('app-release-signed.apk');
expect(util.execute).toHaveBeenCalledWith(test.expectedCwd, androidSdkTools.getEnv(), log);
});
});

it('Throws an error when the APK file name doesn\'t exist', () => {
const fsSpy = spyOn(fs, 'existsSync');
fsSpy.and.returnValue(true);

const config = buildMockConfig(tests[0].platform);
const process = buildMockProcess(tests[0].platform);
const jdkHelper = new JdkHelper(process, config);
const androidSdkTools = new AndroidSdkTools(process, config, jdkHelper);
fsSpy.and.returnValue(false);
expectAsync(androidSdkTools.install('./app-release-signed.apk')).toBeRejectedWithError();
});
});
});