blob: 18d44d7a815b18474c51577ead2b3dd58a0f929a [file] [log] [blame] [edit]
// Copyright 2022 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Script to manage installation of the extension.
*/
import * as fs from 'fs';
import * as path from 'path';
import * as commonUtil from '../common/common_util';
function assertInsideChroot() {
if (!commonUtil.isInsideChroot()) {
throw new Error('not inside chroot');
}
}
const GS_PREFIX = 'gs://chromeos-velocity/ide/cros-ide';
async function execute(name: string, args: string[], showStdout?: boolean) {
return await commonUtil.exec(
name, args, log => process.stderr.write(log), {logStdout: showStdout});
}
/**
* Find specified archive, or the latest one if version is unspecified.
*
* @throws Error if specified version is not found.
*/
async function findArchive(version?: Version): Promise<Archive> {
// The result of `gsutil ls` is lexicographically sorted.
const stdout = await execute('gsutil', ['ls', GS_PREFIX]);
const archives = stdout.trim().split('\n').map(url => {
return Archive.parse(url);
});
archives.sort(Archive.compareFn);
if (!version) {
return archives.pop()!;
}
for (const archive of archives) {
if (compareVersion(archive.version, version) === 0) {
return archive;
}
}
throw new Error(`Version ${versionToString(version)} not found`);
}
// Assert the working directory is clean and get git commit hash.
async function cleanCommitHash() {
if (await execute('git', ['status', '--short'])) {
throw new Error('dirty git status; run the command in clean environment');
}
// IDE_CROS_MAIN_FOR_TESTING substitutes cros/main for manual testing.
const crosMain = process.env.IDE_CROS_MAIN_FOR_TESTING || 'cros/main';
try {
// Assert HEAD is an ancestor of cros/main (i.e. the HEAD is an
// already-merged commit).
await execute('git', ['merge-base', '--is-ancestor', 'HEAD', crosMain]);
} catch (_e) {
throw new Error('HEAD should be an ancestor of cros/main');
}
// HEAD commit should update version in package.json .
const diff = await execute('git',
['diff', '-p', 'HEAD~', '--', '**package.json']);
if (!/^\+\s*"version"\s*:/m.test(diff)) {
throw new Error('HEAD commit should update version in package.json');
}
return await execute('git', ['rev-parse', 'HEAD']);
}
class Archive {
readonly version: Version;
constructor(readonly name: string, readonly hash?: string) {
this.version = versionFromFilename(name);
}
url() {
let res = `${GS_PREFIX}/${this.name}`;
if (this.hash !== undefined) {
res = `${res}@${this.hash}`;
}
return res;
}
static parse(url: string) {
const base = path.basename(url);
const [name, hash] = base.split('@');
return new Archive(name, hash);
}
static compareFn(first: Archive, second: Archive): number {
return compareVersion(first.version, second.version);
}
}
export interface Version {
major: number
minor: number
patch: number
}
function compareVersion(first: Version, second: Version): number {
if (first.major !== second.major) {
return first.major - second.major;
}
if (first.minor !== second.minor) {
return first.minor - second.minor;
}
if (first.patch !== second.patch) {
return first.patch - second.patch;
}
return 0;
}
// Get version from filename such as "cros-ide-0.0.1.vsix"
function versionFromFilename(name: string): Version {
const suffix = name.split('-').pop()!;
const version = suffix.split('.').slice(0, 3).join('.');
return versionFromString(version);
}
/**
* Get version from string such as "0.0.1".
* @throws Error on invalid input.
*/
function versionFromString(s: string): Version {
const version = s.trim().split('.').map(Number);
if (version.length !== 3 || version.some(isNaN)) {
throw new Error(`Invalid version format ${s}`);
}
return {
major: version[0],
minor: version[1],
patch: version[2],
};
}
function versionToString(v: Version): string {
return `${v.major}.${v.minor}.${v.patch}`;
}
async function build(tempDir: string, hash?: string): Promise<Archive> {
await execute('npx', ['vsce@1.103.1', 'package', '-o', `${tempDir}/`]);
const localName: string = (await fs.promises.readdir(tempDir))[0];
return new Archive(localName, hash);
}
export async function buildAndUpload() {
const latestInGs = await findArchive();
const hash = await cleanCommitHash();
await commonUtil.withTempDir(async td => {
const built = await build(td, hash);
if (compareVersion(latestInGs.version, built.version) >= 0) {
throw new Error(
`${built.name} is older than the latest published version ` +
`${latestInGs.name}. Update the version and rerun the program.`);
}
await execute('gsutil', ['cp', path.join(td, built.name), built.url()]);
});
}
export async function installDev() {
await commonUtil.withTempDir(async td => {
const built = await build(td);
const src = path.join(td, built.name);
await execute('code', ['--install-extension', src], true);
});
}
export async function install(forceVersion?: Version) {
const src = await findArchive(forceVersion);
await commonUtil.withTempDir(async td => {
const dst = path.join(td, src.name);
await execute('gsutil', ['cp', src.url(), dst]);
const args = ['--install-extension', dst];
if (forceVersion) {
args.push('--force');
}
await execute('code', args, true);
});
}
interface Config {
forceVersion?: Version
dev?: boolean
upload?: boolean
help?: boolean
}
/**
* Parse args.
*
* @throws Error on invalid input
*/
export function parseArgs(args: string[]): Config {
args = args.slice(); // not to modify the given parameter
while (args.length > 0 && !args[0].startsWith('--')) {
args.shift();
}
const config: Config = {};
while (args.length > 0) {
const flag = args.shift();
switch (flag) {
case '--dev':
config.dev = true;
break;
case '--upload':
config.upload = true;
break;
case '--force':
const s = args.shift();
if (!s) {
throw new Error('Version is not given; see --help');
}
config.forceVersion = versionFromString(s);
break;
case '--help':
config.help = true;
break;
default:
throw new Error(`Unknown flag ${flag}; see --help`);
}
}
if (config.dev && config.upload || config.dev && config.forceVersion ||
config.upload && config.forceVersion) {
throw new Error(`Invalid flag combination; see --help`);
}
return config;
}
const USAGE = `
Usage:
install.sh [options]
Basic options:
--force version
Force install specified version (example: --force 0.0.1)
Without this option, the latest version will be installed.
--help
Print this message
Developer options:
--dev
Build the extension from the current source code and install it
--upload
Build and upload the extension
`;
async function main() {
const config = parseArgs(process.argv);
if (config.help) {
console.log(USAGE);
return;
}
if (config.upload) {
await buildAndUpload();
return;
}
if (config.dev) {
assertInsideChroot();
await installDev();
return;
}
try {
assertInsideChroot();
await install(config.forceVersion);
} catch (e) {
const message = (e as Error).message;
throw new Error(
`${message}\n` +
'Read quickstart.md and run the script in proper environment');
}
}
if (require.main === module) {
main().catch(e => {
console.error(e);
process.exit(1);
});
}