From 841b092d25ab3e7daf28f4c2df8d0d74f73db23d Mon Sep 17 00:00:00 2001 From: nghiaviminh <63698811+nghiaviminh@users.noreply.github.com> Date: Mon, 28 Sep 2020 16:29:58 +0700 Subject: [PATCH] Add .aab installation support (#103) * Add .aab installation support Signed-off-by: nghia.viminh --- Dockerfile | 15 +- lib/cli/storage-temp/index.js | 57 +++++++ lib/units/storage/temp.js | 16 ++ lib/util/bundletool.js | 158 ++++++++++++++++++ package.json | 1 + .../components/stf/install/install-service.js | 2 +- 6 files changed, 244 insertions(+), 5 deletions(-) create mode 100644 lib/util/bundletool.js diff --git a/Dockerfile b/Dockerfile index 29766b82..7bc80eea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,16 +32,21 @@ RUN export DEBIAN_FRONTEND=noninteractive && \ tar -xJf node-v*.tar.xz --strip-components 1 -C /usr/local && \ rm node-v*.tar.xz && \ su stf-build -s /bin/bash -c '/usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js install' && \ - apt-get -y install libzmq3-dev libprotobuf-dev git graphicsmagick yasm && \ + apt-get -y install libzmq3-dev libprotobuf-dev git graphicsmagick openjdk-8-jdk yasm && \ apt-get clean && \ - rm -rf /var/cache/apt/* /var/lib/apt/lists/* + rm -rf /var/cache/apt/* /var/lib/apt/lists/* && \ + mkdir /tmp/bundletool && \ + cd /tmp/bundletool && \ + wget --progress=dot:mega \ + https://github.com/google/bundletool/releases/download/1.2.0/bundletool-all-1.2.0.jar && \ + mv bundletool-all-1.2.0.jar bundletool.jar # Copy app source. COPY . /tmp/build/ # Give permissions to our build user. RUN mkdir -p /app && \ - chown -R stf-build:stf-build /tmp/build /app + chown -R stf-build:stf-build /tmp/build /tmp/bundletool /app # Switch over to the build user. USER stf-build @@ -57,8 +62,10 @@ RUN set -x && \ npm prune --production && \ mv node_modules /app && \ rm -rf ~/.node-gyp && \ + mkdir /app/bundletool && \ + mv /tmp/bundletool/* /app/bundletool && \ cd /app && \ - rm -rf /tmp/* + find /tmp -mindepth 1 ! -regex '^/tmp/hsperfdata_root\(/.*\)?' -delete # Switch to the app user. USER stf diff --git a/lib/cli/storage-temp/index.js b/lib/cli/storage-temp/index.js index c3be46cc..12afcb09 100644 --- a/lib/cli/storage-temp/index.js +++ b/lib/cli/storage-temp/index.js @@ -25,6 +25,52 @@ module.exports.builder = function(yargs) { , type: 'string' , default: os.tmpdir() }) + .option('bundletool-path', { + describe: 'The path to bundletool binary.' + , type: 'string' + , default: '/app/bundletool/bundletool.jar' + }) + .option('ks', { + describe: 'The name of the keystore to sign APKs built from AAB.' + , type: 'string' + , default: 'openstf' + }) + .option('ks-key-alias', { + describe: 'Indicates the alias to be used in the future to refer to the keystore.' + , type: 'string' + , default: 'mykey' + }) + .option('ks-pass', { + describe: 'The password of the keystore.' + , type: 'string' + , default: 'openstf' + }) + .option('ks-key-pass', { + describe: 'The password of the private key contained in keystore.' + , type: 'string' + , default: 'openstf' + }) + .option('ks-keyalg', { + describe: 'The algorithm that is used to generate the key.' + , type: 'string' + , default: 'RSA' + }) + .option('ks-validity', { + describe: 'Number of days of keystore validity.' + , type: 'number' + , default: '90' + }) + .option('ks-keysize', { + describe: 'Key size of the keystore.' + , type: 'number' + , default: '2048' + }) + .option('ks-dname', { + describe: 'Keystore Distinguished Name, contain Common Name(CN), ' + + 'Organizational Unit (OU), Oranization(O), Locality (L), State (S) and Country (C).' + , type: 'string' + , default: 'CN=openstf.io, OU=openstf, O=openstf, L=PaloAlto, S=California, C=US' + }) .epilog('Each option can be be overwritten with an environment variable ' + 'by converting the option to uppercase, replacing dashes with ' + 'underscores and prefixing it with `STF_STORAGE_TEMP_` (e.g. ' + @@ -36,5 +82,16 @@ module.exports.handler = function(argv) { port: argv.port , saveDir: argv.saveDir , maxFileSize: argv.maxFileSize + , bundletoolPath: argv.bundletoolPath + , keystore: { + ksPath: `/tmp/${argv.ks}.keystore` + , ksKeyAlias: argv.ksKeyAlias + , ksPass: argv.ksPass + , ksKeyPass: argv.ksKeyPass + , ksKeyalg: argv.ksKeyalg + , ksValidity: argv.ksValidity + , ksKeysize: argv.ksKeysize + , ksDname: argv.ksDname + } }) } diff --git a/lib/units/storage/temp.js b/lib/units/storage/temp.js index f5df76bb..7e770f52 100644 --- a/lib/units/storage/temp.js +++ b/lib/units/storage/temp.js @@ -13,6 +13,7 @@ var logger = require('../../util/logger') var Storage = require('../../util/storage') var requtil = require('../../util/requtil') var download = require('../../util/download') +var bundletool = require('../../util/bundletool') module.exports = function(options) { var log = logger.createLogger('storage:temp') @@ -91,6 +92,9 @@ module.exports = function(options) { form.uploadDir = options.saveDir } form.on('fileBegin', function(name, file) { + if (/\.aab$/.test(file.name)) { + file.isAab = true + } var md5 = crypto.createHash('md5') file.name = md5.update(file.name).digest('hex') }) @@ -103,9 +107,21 @@ module.exports = function(options) { field: field , id: storage.store(file) , name: file.name + , path: file.path + , isAab: file.isAab } }) }) + .then(function(storedFiles) { + return Promise.all(storedFiles.map(function(file) { + return bundletool({ + bundletoolPath: options.bundletoolPath + , keystore: options.keystore + , file: file + }) + }) + ) + }) .then(function(storedFiles) { res.status(201) .json({ diff --git a/lib/util/bundletool.js b/lib/util/bundletool.js new file mode 100644 index 00000000..b621a7bf --- /dev/null +++ b/lib/util/bundletool.js @@ -0,0 +1,158 @@ +var cp = require('child_process') +var fs = require('fs') +var path = require('path') +var request = require('request') + +var Promise = require('bluebird') +var yauzl = require('yauzl') + +var logger = require('./logger') + +module.exports = function(options) { + return new Promise(function(resolve, reject) { + var log = logger.createLogger('util:bundletool') + var bundletoolFilePath = options.bundletoolPath + var bundle = options.file + var bundlePath = bundle.path + var outputPath = bundlePath + '.apks' + var keystore = options.keystore + + function checkIfJava() { + return new Promise(function(resolve, reject) { + var check = cp.spawn('java', ['-version']) + var stderrChunks = [] + check.on('error', function(err) { + reject(err) + }) + check.stderr.on('data', function(data) { + stderrChunks = stderrChunks.concat(data) + }) + check.stderr.on('end', function() { + var data = Buffer.concat(stderrChunks).toString().split('\n')[0] + var regex = new RegExp('(openjdk|java) version') + var javaVersion = regex.test(data) ? data.split(' ')[2].replace(/"/g, '') : false + if (javaVersion !== false) { + resolve(javaVersion) + } + else { + reject(new Error('Java not found'), null) + } + }) + }) + } + + function convert() { + var proc = cp.spawn('java', [ + '-jar' + , bundletoolFilePath + , 'build-apks' + , `--bundle=${bundlePath}` + , `--output=${outputPath}` + , `--ks=${keystore.ksPath}` + , `--ks-pass=pass:${keystore.ksPass}` + , `--ks-key-alias=${keystore.ksKeyAlias}` + , `--key-pass=pass:${keystore.ksKeyPass}` + , '--overwrite' + , '--mode=universal' + ]) + + proc.on('error', function(err) { + reject(err) + }) + + proc.on('exit', function(code, signal) { + if (signal) { + reject(new Error('Exited with signal ' + signal)) + } + else if (code === 0) { + yauzl.open(outputPath, {lazyEntries: true}, function(err, zipfile) { + if (err) { + reject(err) + } + zipfile.readEntry() + zipfile.on('entry', function(entry) { + if (/\/$/.test(entry.fileName)) { + zipfile.readEntry() + } + else { + zipfile.openReadStream(entry, function(err, readStream) { + if (err) { + reject(err) + } + readStream.on('end', function() { + zipfile.readEntry() + }) + var filePath = entry.fileName.split('/') + var fileName = filePath[filePath.length - 1] + var writeStream = fs.createWriteStream(path.join('/tmp/', fileName)) + writeStream.on('error', function(err) { + reject(err) + }) + readStream.pipe(writeStream) + }) + } + }) + zipfile.on('error', function(err) { + reject(err) + }) + zipfile.once('end', function() { + fs.renameSync('/tmp/universal.apk', bundlePath) + fs.readdirSync('/tmp/', function(err, files) { + if (err) { + reject(err) + } + for (var file of files) { + fs.unlinkSync(path.resolve('/tmp/', file)) + } + fs.unlinkSync(outputPath) + }) + log.info('AAB -> APK') + resolve(bundle) + }) + }) + } + else { + reject(new Error('Exited with status ' + code)) + } + }) + } + + if (bundle.isAab === true) { + log.info('AAB detected') + checkIfJava() + .then(function() { + if (!fs.existsSync(keystore.ksPath)) { + cp.spawnSync('keytool', [ + '-genkey' + , '-noprompt' + , '-keystore', keystore.ksPath + , '-alias', keystore.ksKeyAlias + , '-keyalg', keystore.ksKeyalg + , '-keysize', keystore.ksKeysize + , '-storepass', keystore.ksPass + , '-keypass', keystore.ksKeyPass + , '-dname', keystore.ksDname + , '-validity', keystore.ksValidity + ]) + } + }) + .then(function() { + if(!fs.existsSync(keystore.ksPath)) { + reject('Keystore not found') + } + else if(!fs.existsSync(bundletoolFilePath)) { + reject('bundletool not found') + } + else { + convert() + } + }) + .catch(function(err) { + reject(err) + }) + } + else { + resolve(bundle) + } + }) +} diff --git a/package.json b/package.json index 23254ce2..81f521ef 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "uuid": "^3.0.0", "ws": "^3.0.0", "yargs": "^6.6.0", + "yauzl": "^2.10.0", "zmq": "^2.14.0" }, "devDependencies": { diff --git a/res/app/components/stf/install/install-service.js b/res/app/components/stf/install/install-service.js index cd9902f2..3c7cd98e 100644 --- a/res/app/components/stf/install/install-service.js +++ b/res/app/components/stf/install/install-service.js @@ -92,7 +92,7 @@ module.exports = function InstallService( $rootScope.$broadcast('installation', installation) return StorageService.storeFile('apk', $files, { filter: function(file) { - return /\.apk$/i.test(file.name) + return /\.(apk|aab)$/i.test(file.name) } }) .progressed(function(e) {