mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-24 15:27:17 +00:00
feat: integrate bmap support (#635)
This PR integrates `.bmap` support recently added to `etcher-image-write` into Etcher itself. It does it in the following way: - It adds a `--bmap` option to the Etcher CLI. - It saves a potential `bmap` file contents to the `SelectionStateModel`. - In the GUI, at the time of writing, if there is a `bmap` file content in `SelectionStateModel`, it gets written to a temporary file and such path is passed as the `--bmap` option to the CLI. Since validation checksums don't make sense anymore, the finish screen doesn't show the checksum box in this case. Change-Type: minor Changelog-Entry: Add `.bmap` support. Fixes: https://github.com/resin-io/etcher/issues/171 Signed-off-by: Juan Cruz Viotti <jviottidc@gmail.com> Signed-off-by: Juan Cruz Viotti <jviotti@openmailbox.org>
This commit is contained in:
parent
b69d03a58b
commit
a5a195e8fb
@ -39,6 +39,7 @@ Options
|
||||
--check, -c validate write
|
||||
--robot, -r parse-able output without interactivity
|
||||
--log, -l output log file
|
||||
--bmap, -b bmap file
|
||||
--yes, -y confirm non-interactively
|
||||
--unmount, -u unmount on success
|
||||
```
|
||||
|
@ -120,6 +120,11 @@ module.exports = yargs
|
||||
string: true,
|
||||
alias: 'l'
|
||||
},
|
||||
bmap: {
|
||||
describe: 'bmap file',
|
||||
string: true,
|
||||
alias: 'b'
|
||||
},
|
||||
yes: {
|
||||
describe: 'confirm non-interactively',
|
||||
boolean: true,
|
||||
|
@ -18,6 +18,7 @@
|
||||
|
||||
const _ = require('lodash');
|
||||
const Bluebird = require('bluebird');
|
||||
const fs = Bluebird.promisifyAll(require('fs'));
|
||||
const visuals = require('resin-cli-visuals');
|
||||
const form = require('resin-cli-form');
|
||||
const drivelist = Bluebird.promisifyAll(require('drivelist'));
|
||||
@ -59,8 +60,19 @@ form.run([
|
||||
check: new visuals.Progress('Validating')
|
||||
};
|
||||
|
||||
return drivelist.listAsync().then((drives) => {
|
||||
const selectedDrive = _.find(drives, {
|
||||
return Bluebird.props({
|
||||
drives: drivelist.listAsync(),
|
||||
bmap: _.attempt(() => {
|
||||
if (!options.bmap) {
|
||||
return;
|
||||
}
|
||||
|
||||
return fs.readFileAsync(options.bmap, {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
})
|
||||
}).then((results) => {
|
||||
const selectedDrive = _.find(results.drives, {
|
||||
device: answers.drive
|
||||
});
|
||||
|
||||
@ -70,7 +82,8 @@ form.run([
|
||||
|
||||
return writer.writeImage(options._[0], selectedDrive, {
|
||||
unmountOnSuccess: options.unmount,
|
||||
validateWriteOnSuccess: options.check
|
||||
validateWriteOnSuccess: options.check,
|
||||
bmapContents: results.bmap
|
||||
}, (state) => {
|
||||
|
||||
if (options.robot) {
|
||||
|
@ -37,6 +37,7 @@ const isWindows = os.platform() === 'win32';
|
||||
* @param {Object} options - options
|
||||
* @param {Boolean} [options.unmountOnSuccess=false] - unmount on success
|
||||
* @param {Boolean} [options.validateWriteOnSuccess=false] - validate write on success
|
||||
* @param {String} [options.bmapContents] - bmap file contents
|
||||
* @param {Function} onProgress - on progress callback (state)
|
||||
*
|
||||
* @fulfil {Boolean} - whether the operation was successful
|
||||
@ -60,7 +61,8 @@ exports.writeImage = (imagePath, drive, options, onProgress) => {
|
||||
}).then((image) => {
|
||||
return imageWrite.write(drive, image, {
|
||||
check: options.validateWriteOnSuccess,
|
||||
transform: image.transform
|
||||
transform: image.transform,
|
||||
bmap: options.bmapContents
|
||||
});
|
||||
}).then((writer) => {
|
||||
return new Bluebird((resolve, reject) => {
|
||||
|
@ -260,6 +260,20 @@ SelectionStateModel.service('SelectionStateModel', function(DrivesModel) {
|
||||
return _.get(Store.getState().toJS(), 'selection.image.logo');
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get image bmap
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {String} image bmap
|
||||
*
|
||||
* @example
|
||||
* const imageBmap = SelectionStateModel.getImageBmap();
|
||||
*/
|
||||
this.getImageBmap = () => {
|
||||
return _.get(Store.getState().toJS(), 'selection.image.bmap');
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Check if there is a selected drive
|
||||
* @function
|
||||
|
@ -191,11 +191,7 @@ const storeReducer = (state, action) => {
|
||||
throw new Error('The passedValidation value can\'t be true if the flashing was cancelled');
|
||||
}
|
||||
|
||||
if (action.data.passedValidation && !action.data.sourceChecksum) {
|
||||
throw new Error('Missing results sourceChecksum');
|
||||
}
|
||||
|
||||
if (action.data.passedValidation && !_.isString(action.data.sourceChecksum)) {
|
||||
if (action.data.passedValidation && action.data.sourceChecksum && !_.isString(action.data.sourceChecksum)) {
|
||||
throw new Error(`Invalid results sourceChecksum: ${action.data.sourceChecksum}`);
|
||||
}
|
||||
|
||||
|
@ -26,10 +26,11 @@ const childWriter = require('../../src/child-writer');
|
||||
const MODULE_NAME = 'Etcher.Modules.ImageWriter';
|
||||
const imageWriter = angular.module(MODULE_NAME, [
|
||||
require('../models/settings'),
|
||||
require('../models/selection-state'),
|
||||
require('../models/flash-state')
|
||||
]);
|
||||
|
||||
imageWriter.service('ImageWriterService', function($q, $rootScope, SettingsModel, FlashStateModel) {
|
||||
imageWriter.service('ImageWriterService', function($q, $rootScope, SettingsModel, SelectionStateModel, FlashStateModel) {
|
||||
|
||||
/**
|
||||
* @summary Perform write operation
|
||||
@ -57,7 +58,8 @@ imageWriter.service('ImageWriterService', function($q, $rootScope, SettingsModel
|
||||
return $q((resolve, reject) => {
|
||||
const child = childWriter.write(image, drive, {
|
||||
validateWriteOnSuccess: SettingsModel.get('validateWriteOnSuccess'),
|
||||
unmountOnSuccess: SettingsModel.get('unmountOnSuccess')
|
||||
unmountOnSuccess: SettingsModel.get('unmountOnSuccess'),
|
||||
bmapContents: SelectionStateModel.getImageBmap()
|
||||
});
|
||||
child.on('error', reject);
|
||||
child.on('done', resolve);
|
||||
|
@ -76,6 +76,7 @@ module.exports = function($q, SupportedFormatsModel) {
|
||||
path: imagePath,
|
||||
size: metadata.estimatedSize,
|
||||
name: metadata.name,
|
||||
bmap: metadata.bmap,
|
||||
url: metadata.url,
|
||||
logo: metadata.logo
|
||||
});
|
||||
|
@ -28,7 +28,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="label label-big label-default">CRC32 CHECKSUM : <b class="monospace">{{ ::finish.checksum }}</b></span>
|
||||
<span class="label label-big label-default"
|
||||
ng-if="finish.checksum">CRC32 CHECKSUM : <b class="monospace">{{ ::finish.checksum }}</b></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -107,7 +107,7 @@ module.exports = function(
|
||||
return 'Flash!';
|
||||
}
|
||||
|
||||
if (flashState.percentage === 0) {
|
||||
if (flashState.percentage === 0 && !flashState.speed) {
|
||||
return 'Starting...';
|
||||
} else if (flashState.percentage === 100) {
|
||||
if (isChecking && SettingsModel.get('unmountOnSuccess')) {
|
||||
|
@ -58,7 +58,13 @@ module.exports = function(SupportedFormatsModel, SelectionStateModel, AnalyticsS
|
||||
}
|
||||
|
||||
SelectionStateModel.setImage(image);
|
||||
AnalyticsService.logEvent('Select image', _.omit(image, 'logo'));
|
||||
|
||||
// An easy way so we can quickly identify if we're making use of
|
||||
// certain features without printing pages of text to DevTools.
|
||||
image.logo = Boolean(image.logo);
|
||||
image.bmap = Boolean(image.bmap);
|
||||
|
||||
AnalyticsService.logEvent('Select image', image);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -17,6 +17,9 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
const _ = require('lodash');
|
||||
const Bluebird = require('bluebird');
|
||||
const fs = Bluebird.promisifyAll(require('fs'));
|
||||
const childProcess = require('child_process');
|
||||
const rendererUtils = require('./renderer-utils');
|
||||
const utils = require('./utils');
|
||||
@ -58,10 +61,22 @@ const EXIT_CODES = require('../exit-codes');
|
||||
exports.write = (image, drive, options) => {
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
utils.getTemporaryLogFilePath().then((logFile) => {
|
||||
Bluebird.props({
|
||||
logFile: utils.getTemporaryLogFilePath(),
|
||||
bmapFile: _.attempt(() => {
|
||||
if (!options.bmapContents) {
|
||||
return;
|
||||
}
|
||||
|
||||
return utils.getTemporaryBmapFilePath().tap((bmapFilePath) => {
|
||||
return fs.writeFileAsync(bmapFilePath, options.bmapContents);
|
||||
});
|
||||
})
|
||||
}).then((results) => {
|
||||
const argv = utils.getCLIWriterArguments({
|
||||
entryPoint: rendererUtils.getApplicationEntryPoint(),
|
||||
logFile: logFile,
|
||||
logFile: results.logFile,
|
||||
bmap: results.bmapFile,
|
||||
image: image,
|
||||
device: drive.device,
|
||||
validateWriteOnSuccess: options.validateWriteOnSuccess,
|
||||
@ -70,7 +85,7 @@ exports.write = (image, drive, options) => {
|
||||
|
||||
// Make writer proxy inherit the temporary log file location
|
||||
// while keeping current environment variables intact.
|
||||
process.env[CONSTANTS.TEMPORARY_LOG_FILE_ENVIRONMENT_VARIABLE] = logFile;
|
||||
process.env[CONSTANTS.TEMPORARY_LOG_FILE_ENVIRONMENT_VARIABLE] = results.logFile;
|
||||
|
||||
const child = childProcess.fork(CONSTANTS.WRITER_PROXY_SCRIPT, argv, {
|
||||
silent: true,
|
||||
|
@ -60,6 +60,7 @@ exports.getBooleanArgumentForm = (argumentName, value) => {
|
||||
* @param {String} options.device - device
|
||||
* @param {String} options.entryPoint - entry point
|
||||
* @param {String} [options.logFile] - log file
|
||||
* @param {String} [options.bmap] - bmap file
|
||||
* @param {Boolean} [options.validateWriteOnSuccess] - validate write on success
|
||||
* @param {Boolean} [options.unmountOnSuccess] - unmount on success
|
||||
* @returns {String[]} arguments
|
||||
@ -107,6 +108,10 @@ exports.getCLIWriterArguments = (options) => {
|
||||
argv.push('--log', options.logFile);
|
||||
}
|
||||
|
||||
if (options.bmap) {
|
||||
argv.push('--bmap', options.bmap);
|
||||
}
|
||||
|
||||
return argv;
|
||||
};
|
||||
|
||||
@ -146,3 +151,23 @@ exports.getTemporaryLogFilePath = () => {
|
||||
postfix: '.log'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get a temporary bmap file path
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @fulfil {String} - bmap path
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* utils.getTemporaryBmapFilePath().then((bmapFilePath) => {
|
||||
* console.log(bmapFilePath);
|
||||
* });
|
||||
*/
|
||||
exports.getTemporaryBmapFilePath = () => {
|
||||
return tmp.fileAsync({
|
||||
prefix: `${packageJSON.name}-`,
|
||||
postfix: '.bmap'
|
||||
});
|
||||
};
|
||||
|
43
npm-shrinkwrap.json
generated
43
npm-shrinkwrap.json
generated
@ -398,6 +398,43 @@
|
||||
"from": "bluebird-retry@>=0.7.0 <0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/bluebird-retry/-/bluebird-retry-0.7.0.tgz"
|
||||
},
|
||||
"bmapflash": {
|
||||
"version": "1.1.2",
|
||||
"from": "bmapflash@>=1.1.2 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bmapflash/-/bmapflash-1.1.2.tgz",
|
||||
"dependencies": {
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"from": "isarray@~1.0.0",
|
||||
"resolved": "http://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.15.0",
|
||||
"from": "lodash@>=4.14.2 <5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.15.0.tgz"
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.0.6",
|
||||
"from": "readable-stream@~2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz"
|
||||
},
|
||||
"through2": {
|
||||
"version": "2.0.1",
|
||||
"from": "through2@^2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.1.tgz"
|
||||
},
|
||||
"xml2js": {
|
||||
"version": "0.4.17",
|
||||
"from": "xml2js@>=0.4.17 <0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.17.tgz"
|
||||
},
|
||||
"xtend": {
|
||||
"version": "4.0.1",
|
||||
"from": "xtend@~4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"bn.js": {
|
||||
"version": "4.11.4",
|
||||
"from": "bn.js@>=4.1.1 <5.0.0",
|
||||
@ -1371,9 +1408,9 @@
|
||||
}
|
||||
},
|
||||
"etcher-image-write": {
|
||||
"version": "6.0.1",
|
||||
"from": "etcher-image-write@6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/etcher-image-write/-/etcher-image-write-6.0.1.tgz",
|
||||
"version": "6.1.1",
|
||||
"from": "etcher-image-write@6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/etcher-image-write/-/etcher-image-write-6.1.1.tgz",
|
||||
"dependencies": {
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"drivelist": "^3.2.6",
|
||||
"electron-is-running-in-asar": "^1.0.0",
|
||||
"etcher-image-stream": "^3.1.0",
|
||||
"etcher-image-write": "^6.0.1",
|
||||
"etcher-image-write": "^6.1.1",
|
||||
"etcher-latest-version": "^1.0.0",
|
||||
"file-tail": "^0.3.0",
|
||||
"flexboxgrid": "^6.3.0",
|
||||
|
@ -338,13 +338,13 @@ describe('Browser: FlashStateModel', function() {
|
||||
}).to.throw('Invalid results cancelled: false');
|
||||
});
|
||||
|
||||
it('should throw if passedValidation is true and sourceChecksum does not exist', function() {
|
||||
it('should not throw if passedValidation is true and sourceChecksum does not exist', function() {
|
||||
m.chai.expect(function() {
|
||||
FlashStateModel.unsetFlashingFlag({
|
||||
passedValidation: true,
|
||||
cancelled: false
|
||||
});
|
||||
}).to.throw('Missing results sourceChecksum');
|
||||
}).to.not.throw();
|
||||
});
|
||||
|
||||
it('should throw if passedValidation is true and sourceChecksum is not a string', function() {
|
||||
|
@ -55,6 +55,10 @@ describe('Browser: SelectionState', function() {
|
||||
m.chai.expect(SelectionStateModel.getImageLogo()).to.be.undefined;
|
||||
});
|
||||
|
||||
it('getImageBmap() should return undefined', function() {
|
||||
m.chai.expect(SelectionStateModel.getImageBmap()).to.be.undefined;
|
||||
});
|
||||
|
||||
it('hasDrive() should return false', function() {
|
||||
const hasDrive = SelectionStateModel.hasDrive();
|
||||
m.chai.expect(hasDrive).to.be.false;
|
||||
@ -272,7 +276,8 @@ describe('Browser: SelectionState', function() {
|
||||
size: 999999999,
|
||||
url: 'https://www.raspbian.org',
|
||||
name: 'Raspbian',
|
||||
logo: '<svg><text fill="red">Raspbian</text></svg>'
|
||||
logo: '<svg><text fill="red">Raspbian</text></svg>',
|
||||
bmap: '<Range>Foo Bar</Range>'
|
||||
});
|
||||
});
|
||||
|
||||
@ -425,6 +430,15 @@ describe('Browser: SelectionState', function() {
|
||||
|
||||
});
|
||||
|
||||
describe('.getImageBmap()', function() {
|
||||
|
||||
it('should return the image bmap', function() {
|
||||
const imageBmap = SelectionStateModel.getImageBmap();
|
||||
m.chai.expect(imageBmap).to.equal('<Range>Foo Bar</Range>');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('.hasImage()', function() {
|
||||
|
||||
it('should return true', function() {
|
||||
|
@ -177,6 +177,22 @@ describe('Browser: MainPage', function() {
|
||||
FlashStateModel.setFlashingFlag();
|
||||
});
|
||||
|
||||
it('should report 0% if percentage == 0 but speed != 0', function() {
|
||||
const controller = $controller('FlashController', {
|
||||
$scope: {}
|
||||
});
|
||||
|
||||
FlashStateModel.setProgressState({
|
||||
type: 'write',
|
||||
percentage: 0,
|
||||
eta: 15,
|
||||
speed: 100000000000000
|
||||
});
|
||||
|
||||
SettingsModel.set('unmountOnSuccess', true);
|
||||
m.chai.expect(controller.getProgressButtonLabel()).to.equal('0%');
|
||||
});
|
||||
|
||||
it('should handle percentage == 0, type = write, unmountOnSuccess', function() {
|
||||
const controller = $controller('FlashController', {
|
||||
$scope: {}
|
||||
@ -186,7 +202,7 @@ describe('Browser: MainPage', function() {
|
||||
type: 'write',
|
||||
percentage: 0,
|
||||
eta: 15,
|
||||
speed: 1000
|
||||
speed: 0
|
||||
});
|
||||
|
||||
SettingsModel.set('unmountOnSuccess', true);
|
||||
@ -202,7 +218,7 @@ describe('Browser: MainPage', function() {
|
||||
type: 'write',
|
||||
percentage: 0,
|
||||
eta: 15,
|
||||
speed: 1000
|
||||
speed: 0
|
||||
});
|
||||
|
||||
SettingsModel.set('unmountOnSuccess', false);
|
||||
@ -218,7 +234,7 @@ describe('Browser: MainPage', function() {
|
||||
type: 'check',
|
||||
percentage: 0,
|
||||
eta: 15,
|
||||
speed: 1000
|
||||
speed: 0
|
||||
});
|
||||
|
||||
SettingsModel.set('unmountOnSuccess', true);
|
||||
@ -234,7 +250,7 @@ describe('Browser: MainPage', function() {
|
||||
type: 'check',
|
||||
percentage: 0,
|
||||
eta: 15,
|
||||
speed: 1000
|
||||
speed: 0
|
||||
});
|
||||
|
||||
SettingsModel.set('unmountOnSuccess', false);
|
||||
|
Loading…
x
Reference in New Issue
Block a user