UNPKG

flash-b2g

Version:

Shallow-flash Gecko and Gaia on Firefox OS devices from Mozilla's public build server.

420 lines (386 loc) 11.9 kB
#! /usr/bin/env node 'use strict'; var http = require('http'); var path = require('path'); var fs = require('fs'); var childProcess = require('child_process'); var JSFtp = require('jsftp'); var Promise = require('es6-promise').Promise; var temp = require('temp'); var mkdirp = require('mkdirp'); var moment = require('moment'); var yargs = require('yargs'); var colors = require('colors'); // Helper function promisify(fn) { var args = Array.prototype.slice.call(arguments, 1); return new Promise(function(resolve, reject) { args.push(function(err) { if (err != null) { reject(err); } else { resolve.apply(null, Array.prototype.slice.call(arguments, 1)); } }); if (Array.isArray(fn)) { var scope = fn[0]; var method = fn[1]; scope[method].apply(scope, args); } else { fn.apply(null, args); } }); } // Command-line arguments var yarg = yargs.usage('Shallow-flash Gecko and Gaia on Firefox OS devices from Mozilla\'s public build server (http://ftp.mozilla.org/pub/mozilla.org/b2g/nightly/).\nUsage: flash-b2g [device] [channel=central]') .example('$0 flame-kk 2.0', 'Flash a flame with 2.0 build.') .example('$0 flame-kk --folder ~/', 'Flash a flame with a nightly build (downloaded to ~/)') .example('$0 flame-kk --folder ~/ --local', 'Flash a Flame device with a previously downloaded build in ~/.') .example('$0 hamachi aurora --eng', 'Flash an Hamachi device with an aurora engineering build.') .string([1, 'device', 'channel', 'date']) .alias({ 'dir': 'd', 'channel': 'c', 'device': 'i', 'date': 't', 'local': 'l', 'eng': 'e', 'profile': 'p', 'remotify': 'r', 'help': 'h' }) .boolean(['eng', 'local', 'profile', 'help', 'remotify', 'help', 'only-remotify']) .default({ channel: 'central', date: 'latest' }) .describe({ device: 'Device (flame-kk [kitkat base image], flame, helix, hamachi, …)', channel: 'Channel (central, aurora, 1.4, …)', date: 'Build date (for regression window testing)', eng: 'Engineering build (for marionette testing)', dir: 'Directory to keep downloads (defaults to temp)', local: 'Use local files, skipping FTP (requires --dir)', profile: 'Keep profile (no promises)', remotify: 'Set device into development mode', 'only-remotify': 'Skip flashing, only set development mode', help: 'Show this help' }) .strict(); var argv = yarg.argv; argv.device = (argv._[0] || argv.device || '').toLowerCase(); if (argv.help || (!argv.device && !argv['only-remotify'])) { console.log(yarg.help()); if (!argv.device) { console.log('Please provide a [device] argument!'); } process.exit(); } argv.channel = String(argv._[1] || argv.channel); if (/^\d+$/.test(argv.channel)) { argv.channel += '.0'; } if (argv.date && argv.date != 'latest') { argv.date = moment(argv.date); if (!argv.date.isValid()) { throw new Error('Invalid `date` argument'); } } else { argv.date = null; } var env = process.env; // Constants var FTP_HOST = 'ftp.mozilla.org'; var FTP_URL = 'http://' + FTP_HOST; var FTP_PATH = '/pub/mozilla.org/b2g/nightly/'; var SCRIPT_PATH = path.join(__dirname, 'scripts'); var FLASH_SCRIPT_PATH = path.join(SCRIPT_PATH, 'shallow_flash.sh'); var BACKUP_SCRIPT_PATH = path.join(SCRIPT_PATH, 'backup_restore_profile.sh'); var TIMEOUT = 60 * 60 * 1000; // 1min var ADB = process.env.ADB || 'adb'; // Set up dirs temp.track(); var tempDir = temp.mkdirSync('flash-b2g'); var dir = argv.dir; if (!dir) { dir = tempDir; } else { dir = path.resolve(dir) if (!fs.existsSync(dir)) { mkdirp.sync(dir); console.log('Created directory: %s', dir) } } var profileDir = path.join(dir, 'flash-b2g-profile'); // At least on linux bash, getopt defines that optional arguments need to be // like "--opt=arg". Only required arguments can do "--opt arg". var argsSpace = (process.platform == 'linux') ? '=' : ' '; if (argv['only-remotify']) { setDeveloperPrefs() .then(function() { console.log('✓ Done (only-remotify)'); }, function(err) { console.error(err); }) .then(function() { process.exit(); }); return; } var dateBit = (argv.date) ? argv.date.format('YYYY-MM-DD') : 'latest'; var channelBit = argv.channel; if (/\d/.test(channelBit)) { channelBit = '.*v' + channelBit.replace(/\./, '_'); } var pathBits = [dateBit, '.*', channelBit, argv.device]; var nameBits = [argv.device, channelBit]; if (argv.eng) { pathBits.push('eng'); nameBits.push('eng'); } nameBits.push(dateBit); var pathMatch = new RegExp('^' + pathBits.join('-') + '$', 'i'); var ftpPath = FTP_PATH; // Strip the regex bits that are filesystem wildcards or otherwise weird out. var safeNameBits = nameBits.join('-').replace(/[^\w_-]+/gi, '_'); var localGaiaPath = path.join(dir, 'gaia-' + safeNameBits + '.zip'); var localB2gPath = path.join(dir, 'b2g-' + safeNameBits + '.tar.gz'); function setDeveloperPrefs() { var prefs = { 'devtools.debugger.forbid-certified-apps': false, 'devtools.debugger.prompt-connection': false, 'b2g.adb.timeout': 0, // Reduce noise in logs: http://kb.mozillazine.org/Layout.css.report_errors 'layout.css.report_errors': false }; var settings = { 'developer.menu.enabled': true, 'ftu.manifestURL': null, 'debugger.remote-mode': 'adb-devtools', 'devtools.debugger.remote-enabled': true, 'screen.timeout': 600, // 10min 'lockscreen.locked': false, 'lockscreen.enabled': false }; // Wait for device console.log('Waiting for device (is remote debugging on?) …'.yellow); return promisify(childProcess.exec, ADB + ' wait-for-device') // Stop B2G .then(function stopB2g() { console.log('Stopping system'); return promisify(childProcess.exec, [ ADB + ' remount', // really needed? ADB + ' shell stop b2g' ].join(' && ')); }) // Fetch settings.json .then(function pullSettings() { return promisify(childProcess.exec, ADB + ' shell cat /system/b2g/defaults/settings.json', { maxBuffer: 524288 }); }) .then(function pushSettings(stdout) { var content = JSON.parse(stdout); for (var key in settings) { content[key] = settings[key]; } var settingsPath = path.join(tempDir, 'settings.json'); fs.writeFileSync(settingsPath, JSON.stringify(content)); console.log('Appending to settings.json:\n', settings); return promisify(childProcess.exec, [ ADB + ' shell mount -o rw,remount /system', ADB + ' push ' + settingsPath + ' /system/b2g/defaults/settings.json', ADB + ' shell mount -o ro,remount /system' ].join(' && ')); }) // Push preferences .then(function pushPreferences() { var cmds = ['cd /data/b2g/mozilla/*.default/'] .concat(Object.keys(prefs).map(function(key) { return 'echo \'user_pref(' + JSON.stringify(key) + ', ' + JSON.stringify(prefs[key]) + ');\' >> prefs.js'; })).join(' && '); console.log('Appending to prefs.js:\n', prefs); return promisify(childProcess.exec, ADB + ' shell "' + cmds.replace(/"/g, '\\"') + '"', { maxBuffer: 524288 }); }) // Restart B2G .then(function restartB2g() { console.log('Restarting system'); return promisify(childProcess.exec, ADB + ' shell sync && ' + ADB + ' shell start b2g') }); }; function download() { // FTP connection // TODO: Allow login var ftp = new JSFtp({ host: FTP_HOST }); ftp.useList = true; ftp.socket.setTimeout(TIMEOUT); ftp.socket.once('timeout', function(data) { ftp.socket.destroy(); // should call the "ls" callback "err" }); console.log('Connecting to %s', FTP_HOST.underline.white); // List files in main dir return promisify([ftp, 'ls'], ftpPath) // Find target dir .then(function findTargetFolder(files) { files = files.filter(function(file) { return pathMatch.test(file.name); }); if (!files.length) { throw new Error('Could not find a directory matching ' + pathMatch + '.\nVerify your --device, --channel or --date argument and that a matching directory exists on the Mozilla FTP:\n' + FTP_URL + ftpPath); } return files[0].name; }) // List files in target dir .then(function listTargetFiles(path) { ftpPath += path + '/'; console.log('Listing files from %s', (FTP_HOST.underline + ftpPath).white); return promisify([ftp, 'ls'], ftpPath); }) // Find b2g package .then(function findB2gFile(files) { files = files.filter(function(file) { return /android-arm\.tar\.gz$/.test(file.name); }); if (!files.length) { throw new Error('Could not find b2g package!'); } return files[0].name; }) // Open sockets for b2g and gaia packages .then(function openStreams(file) { return Promise.all([ new Promise(function(resolve, reject) { http.get(FTP_URL + ftpPath + 'gaia.zip', resolve) .on('error', reject); }), new Promise(function(resolve, reject) { http.get(FTP_URL + ftpPath + file, resolve) .on('error', reject); }) ]); }) // Stream socket data into files .then(function downloadStreams(streams) { console.log('Downloading from %s', (FTP_URL + ftpPath).underline.white); var fileFlags = { flags: 'w' }; return Promise.all([ new Promise(function(resolve, reject) { var file = fs.createWriteStream(localGaiaPath, fileFlags); file.on('finish', function() { file.close(resolve); }); streams[0].pipe(file); }), new Promise(function(resolve, reject) { var file = fs.createWriteStream(localB2gPath, { flags: 'w' }); file.on('finish', function() { file.close(resolve); }); streams[1].pipe(file); }) ]); }); } // Start process Promise.resolve().then(function() { if (argv.local) { console.log('Skipping FTP'); return; } return download(); }) .then(function() { if (!fs.existsSync(localGaiaPath) || !fs.existsSync(localB2gPath)) { throw new Error('Local files not found:\n' + localGaiaPath + '\n' + localB2gPath); } }) .then(function() { console.log('Waiting for device (is remote debugging on?) …'.yellow); return promisify(childProcess.exec, ADB + ' wait-for-device') }) .then(function() { if (argv.profile) { console.log('Exec ' + 'backup_restore_profile.sh'.grey.italic + ' to back up'); return promisify(childProcess.exec, [ BACKUP_SCRIPT_PATH, '-p' + argsSpace + profileDir, '-b' ].join(' '), { cwd: SCRIPT_PATH }) .then(function(stdout, stderr) { if (stdout) { console.log('[backup_restore_profile] '.grey + String(stdout).trim()); } if (stderr) { console.log('[backup_restore_profile] '.red + String(stderr).trim()); } }); } }) // Execute flash script .then(function executeFlash(stdout) { var args = [ '--gaia' + argsSpace + localGaiaPath, '--gecko' + argsSpace + localB2gPath, '-y' ].join(' '); console.log('Exec ' + ('shallow_flash.sh ' + args).grey.italic); console.log('This can take a bit, finishing with the device restarting …'.yellow); return promisify(childProcess.exec, FLASH_SCRIPT_PATH + ' ' + args, { cwd: SCRIPT_PATH }) .then(function(stdout, stderr) { if (stdout) { console.log('[shallow_flash] '.grey + String(stdout).trim()); } if (stderr) { console.log('[shallow_flash] '.red + String(stderr).trim()); } }); }) .then(function() { if (argv.profile) { console.log('Exec ' + 'backup_restore_profile.sh'.grey.italic + ' to restore'); return promisify(childProcess.exec, [ BACKUP_SCRIPT_PATH, '-p' + argsSpace + profileDir, '--no-reboot', '-r' ].join(' '), { cwd: SCRIPT_PATH }) .then(function(stdout, stderr) { if (stdout) { console.log('[backup_restore_profile] '.grey + String(stdout).trim()); } if (stderr) { console.log('[backup_restore_profile] '.red + String(stderr).trim()); } }); } }) // Set developer prefs .then(function() { if (argv.remotify) { return setDeveloperPrefs(); } }) // Listo! .then(function() { console.log('✓ %s flashed to %s!'.bold.green, argv.device, argv.channel); process.exit(); }, function(err) { console.error(String(err).bold.red, err); process.exit(1); });