Skip to content

Commit

Permalink
Merge pull request #650 from particle-iot/fix/get-assets-from-propert…
Browse files Browse the repository at this point in the history
…ies-file

Rework particle bundle command
  • Loading branch information
keeramis authored Jun 29, 2023
2 parents dd0783f + d69e42f commit 328a903
Show file tree
Hide file tree
Showing 21 changed files with 262 additions and 64 deletions.
15 changes: 8 additions & 7 deletions src/cli/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,23 @@ module.exports = ({ commandProcessor, root }) => {
params: '<appBinary>',
options: {
'saveTo': {
description: 'Filename for the compiled binary'
description: 'Specify the filename for the compiled binary'
},
'assets': {
description: 'The folder path of assets to be bundled'
description: 'Optional. Specify the assets directory using --assets /path/to/assets or --assets /path/to/project.properties. If not specified, assets are obtained from the assetOtaDir property in the project.properties file'
}
},
handler: (args) => {
const BundleCommands = require('../cmd/bundle');
return new BundleCommands().createBundle(args);
},
examples: {
'$0 $command myApp.bin --assets /path/to/assets': 'Creates a bundle of application binary and assets from the /path/to/assets folder',
'$0 $command myApp.bin': 'Creates a bundle of application binary and assets from the default /assets folder in the current directory if available',
'$0 $command myApp.bin --assets /path/to/assets --saveTo myApp.zip': 'Creates a bundle of application binary and assets from the /path/to/assets folder and saves it to the myApp.zip file',
'$0 $command myApp.bin --saveTo myApp.zip': 'Creates a bundle of application binary and assets from the default /assets folder in the current directory if available, and saves the bundle to the myApp.zip file'
'$0 $command myApp.bin': 'Creates a bundle of application binary and assets. The assets are obtained from the project.properties in the current directory',
'$0 $command myApp.bin --assets /path/to/assets': 'Creates a bundle of application binary and assets. The assets are obtained from /path/to/assets directory',
'$0 $command myApp.bin --assets /path/to/project.properties': 'Creates a bundle of application binary and assets. The assets are picked up from the provided project.properties file',
'$0 $command myApp.bin --assets /path/ --saveTo myApp.zip': 'Creates a bundle of application binary and assets, and saves it to the myApp.zip file',
'$0 $command myApp.bin --saveTo myApp.zip': 'Creates a bundle of application binary and assets as specified in the assetOtaDir if available, and saves the bundle to the myApp.zip file'
},
epilogue: 'If --assets option is not specified, the folder named \'assets\' in the current directory is used'
epilogue: 'Add assetOtaDir=assets to your project.properties file to bundle assets from the asset directory. The assets path should be relative to the project root.'
});
};
15 changes: 8 additions & 7 deletions src/cli/bundle.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ const helpCommandOutput = [
'Usage: particle bundle [options] <appBinary>',
'',
'Options:',
' --saveTo Filename for the compiled binary [string]',
' --assets The folder path of assets to be bundled [string]',
' --saveTo Specify the filename for the compiled binary [string]',
' --assets Optional. Specify the assets directory using --assets /path/to/assets or --assets /path/to/project.properties. If not specified, assets are obtained from the assetOtaDir property in the project.properties file [string]',
'',
'Examples:',
' particle bundle myApp.bin --assets /path/to/assets Creates a bundle of application binary and assets from the /path/to/assets folder',
' particle bundle myApp.bin Creates a bundle of application binary and assets from the default /assets folder in the current directory if available',
' particle bundle myApp.bin --assets /path/to/assets --saveTo myApp.zip Creates a bundle of application binary and assets from the /path/to/assets folder and saves it to the myApp.zip file',
' particle bundle myApp.bin --saveTo myApp.zip Creates a bundle of application binary and assets from the default /assets folder in the current directory if available, and saves the bundle to the myApp.zip file',
' particle bundle myApp.bin Creates a bundle of application binary and assets. The assets are obtained from the project.properties in the current directory',
' particle bundle myApp.bin --assets /path/to/assets Creates a bundle of application binary and assets. The assets are obtained from /path/to/assets directory',
' particle bundle myApp.bin --assets /path/to/project.properties Creates a bundle of application binary and assets. The assets are picked up from the provided project.properties file',
' particle bundle myApp.bin --assets /path/ --saveTo myApp.zip Creates a bundle of application binary and assets, and saves it to the myApp.zip file',
' particle bundle myApp.bin --saveTo myApp.zip Creates a bundle of application binary and assets as specified in the assetOtaDir if available, and saves the bundle to the myApp.zip file',
'',
'If --assets option is not specified, the folder named \'assets\' in the current directory is used',
'Add assetOtaDir=assets to your project.properties file to bundle assets from the asset directory. The assets path should be relative to the project root.',
''
].join('\n');

Expand Down
2 changes: 1 addition & 1 deletion src/cli/library.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ module.exports = ({ commandProcessor, root }) => {
'copy': {
boolean: true,
alias: 'vendored',
description: 'install the library by copying the library sources into the project\'s lib folder'
description: 'install the library by copying the library sources into the project\'s lib directory'
},
'adapter': { // hidden
boolean: true,
Expand Down
47 changes: 40 additions & 7 deletions src/cmd/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,53 @@ module.exports = class BundleCommands extends CLICommandBase {
throw new Error(`The target file ${saveTo} must be a .zip file`);
}

// If no assets folder is specified, use the default assets folder in the current directory
let assetsPath = assets ? assets : 'assets';

let assetsPath = await this._getAssetsPath(assets);
const bundleFilename = this._getBundleSavePath(saveTo, appBinary);
return { assetsPath, bundleFilename };
}

async _getAssetsPath(assets) {
if (assets) {
if (await fs.exists(assets)) {
// check if assets is a project.properties file
const stat = await fs.stat(assets);
if (stat.isFile() && utilities.getFilenameExt(assets) === '.properties') {
return this._getAssetsPathFromProjectProperties(assets);
} else {
return assets;
}
}
throw new Error(`The assets dir ${assets} does not exist`);
}
const projectPropertiesPath = path.join(process.cwd(), 'project.properties');
return this._getAssetsPathFromProjectProperties(projectPropertiesPath);
}

async _getAssetsPathFromProjectProperties(projectPropertiesPath) {
if (!await fs.exists(projectPropertiesPath)) {
throw new Error('No project.properties file found in the current directory. ' +
'Please specify the assets directory using --assets option');
}
const propFile = await utilities.parsePropertyFile(projectPropertiesPath);
if (propFile.assetOtaDir && propFile.assetOtaDir !== '') {
// get the assets dir relative to the project.properties file
return path.join(path.dirname(projectPropertiesPath), propFile.assetOtaDir);
} else if (!propFile.assetOtaDir) {
throw new Error('Add assetOtaDir to your project.properties in order to bundle assets');
}
}

async _getAssets({ assetsPath }) {
if (!await fs.exists(assetsPath)) {
throw new Error(`The assets folder ${assetsPath} does not exist`);
throw new Error(`The assets dir ${assetsPath} does not exist`);
}
const fileStat = await fs.stat(assetsPath);
if (!fileStat.isDirectory()) {
throw new Error(`The assets path ${assetsPath} is not a directory`);
}
// Only get the assets from the folder itself, ignoring any sub-folders
const assetsInFolder = await fs.readdir(assetsPath);
const assetFiles = await Promise.all(assetsInFolder.map(async (f) => {
// Only get the assets from the dir itself, ignoring any sub-dir
const assetsInDir = await fs.readdir(assetsPath);
const assetFiles = await Promise.all(assetsInDir.map(async (f) => {
const filepath = path.join(assetsPath, f);
const stat = await fs.stat(filepath);
if (stat.isDirectory() || f.startsWith('.') || specialFiles.includes(f)) {
Expand Down
94 changes: 85 additions & 9 deletions src/cmd/bundle.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ describe('BundleCommands', () => {

it('returns a .zip file', async () => {
const binPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid', 'app.bin');
const assetsPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid', 'assets');
const assetsPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid', 'otaAssets');
const args = {
params: {
appBinary: binPath,
Expand All @@ -110,7 +110,7 @@ describe('BundleCommands', () => {
expect(bundleFilename).to.eq(targetBundlePath);
});

it('uses the assets in the assets folder when --assets option is not specified', async () => {
it('uses the assets from the project.properties file when --assets option is not specified', async () => {
const binPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid', 'app.bin');
const assetsPath = undefined;
const args = {
Expand All @@ -128,9 +128,9 @@ describe('BundleCommands', () => {
});
});

it('uses the assets in the assets folder when --assets option is specified', async () => {
it('uses the assets in the assets dir when --assets option is specified', async () => {
const binPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid', 'app.bin');
const assetsPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid', 'assets');
const assetsPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid', 'otaAssets');
const args = {
params: {
appBinary: binPath,
Expand All @@ -144,7 +144,7 @@ describe('BundleCommands', () => {
expect(bundleFilename).to.eq(targetBundlePath);
});

it('creates a bundle if there are no assets in the assets folder', async () => {
it('creates a bundle if there are no assets in the assets dir', async () => {
const binPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'zero_assets', 'app.bin');
const assetsPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'zero_assets', 'assets');
const args = {
Expand All @@ -162,7 +162,7 @@ describe('BundleCommands', () => {

it('returns bundle with the default name if saveTo argument is not provided', async () => {
const binPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid', 'app.bin');
const assetsPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid', 'assets');
const assetsPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid', 'otaAssets');
const args = {
params: {
appBinary: binPath,
Expand All @@ -182,7 +182,7 @@ describe('BundleCommands', () => {
});

describe('getAssets', () => {
it('throws an error when assets folder is not present', async () => {
it('throws an error when assets dir is not present', async () => {
const assetsPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'invalid_no_assets', 'assets');
let error;

Expand All @@ -193,11 +193,11 @@ describe('BundleCommands', () => {
}

expect(error).to.be.an.instanceof(Error);
expect(error.message).to.eql(`The assets folder ${assetsPath} does not exist`);
expect(error.message).to.eql(`The assets dir ${assetsPath} does not exist`);
});

it('returns the assets list', async () => {
const assetsPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid', 'assets');
const assetsPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid', 'otaAssets');

const assetsList = await bundleCommands._getAssets({ assetsPath });

Expand Down Expand Up @@ -241,4 +241,80 @@ describe('BundleCommands', () => {
expect(res).to.match(/^bundle_test_\d+\.zip$/);
});
});

describe('_getAssetsPath', () => {
it('returns assets directory if --assets directory is provided', async () => {
await runInDirectory(path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid'), async () => {
const assetsPath = await bundleCommands._getAssetsPath(path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid', 'otaAssets'));
expect(assetsPath).to.equal(path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid', 'otaAssets'));
});
});

it ('returns path from project.properties if --assets path/to/project.properties is provided', async () => {
await runInDirectory(path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid'), async () => {
const assetsPath = await bundleCommands._getAssetsPath(undefined);
expect(assetsPath).to.equal(path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid', 'otaAssets'));
});
});

it('returns error if --assets is not provided and project.properties is not present', async () => {
await runInDirectory(path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid-no-proj-prop'), async () => {
let error;
try {
await bundleCommands._getAssetsPath(undefined);
} catch (_error) {
error = _error;
}

expect(error).to.be.an.instanceof(Error);
expect(error.message).to.eql('No project.properties file found in the current directory. Please specify the assets directory using --assets option');
});
});

it('returns assets from project.properties even if project.properties is not specified', async () => {
await runInDirectory(path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid'), async () => {
const assetsPath = await bundleCommands._getAssetsPath(undefined);

expect(assetsPath).to.equal(path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid', 'otaAssets'));
});
});
});

describe('_getAssetsPathFromProjectProperties', () => {
it('returns the value of assetOtaDir property', async () => {
await runInDirectory(path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid'), async () => {
const assetsPath = await bundleCommands._getAssetsPathFromProjectProperties('project.properties');

expect(assetsPath).to.equal('otaAssets');
});
});

it('returns undefined if project.properties file is not present', async () => {
await runInDirectory(path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid-no-proj-prop'), async () => {
let error;
try {
await bundleCommands._getAssetsPathFromProjectProperties(undefined);
} catch (_error) {
error = _error;
}

expect(error).to.be.an.instanceof(Error);
expect(error.message).to.eql('No project.properties file found in the current directory. Please specify the assets directory using --assets option');
});
});

it('returns undefined if assetOtaDir property is not present', async () => {
await runInDirectory(path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid-no-prop'), async () => {
let error;
try {
await bundleCommands._getAssetsPathFromProjectProperties('project.properties');
} catch (_error) {
error = _error;
}

expect(error).to.be.an.instanceof(Error);
expect(error.message).to.eql('Add assetOtaDir to your project.properties in order to bundle assets');
});
});
});
});
16 changes: 5 additions & 11 deletions src/cmd/cloud.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ const os = require('os');
const _ = require('lodash');
const VError = require('verror');
const prompt = require('inquirer').prompt;
const propertiesParser = require('properties-parser');

const settings = require('../../settings');
const deviceSpecs = require('../lib/device-specs');
Expand Down Expand Up @@ -690,18 +689,13 @@ module.exports = class CloudCommand extends CLICommandBase {
for (const file of files) {
const propPath = path.join(file, 'project.properties');
try {
const savedProp = await fs.readFile(propPath, 'utf8');
const savedPropObj = propertiesParser.parse(savedProp);

if (savedPropObj.assetOtaFolder && savedPropObj.assetOtaFolder !== '') {
const assetsDir = path.join(file, savedPropObj.assetOtaFolder);
const stats = await fs.stat(assetsDir);
if (stats.isDirectory()) {
return assetsDir;
}
const savedPropObj = await utilities.parsePropertyFile(propPath);

if (savedPropObj.assetOtaDir && savedPropObj.assetOtaDir !== '') {
return path.join(file, savedPropObj.assetOtaDir);
}
} catch (error) {
// Ignore file read or stat errors
// Ignore parsing or stat errors
}
}
}
Expand Down
10 changes: 5 additions & 5 deletions src/cmd/cloud.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ describe('Cloud Commands', () => {
it('returns path to assets folder', async () => {
const { cloud } = stubForLogin(new CloudCommands(), stubs);
const dirPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid');
expect(await cloud._checkForAssets([dirPath])).to.equal(path.join(dirPath, 'assets'));
expect(await cloud._checkForAssets([dirPath])).to.equal(path.join(dirPath, 'otaAssets'));
});

it('returns undefined if assets folder is missing', async () => {
Expand All @@ -288,7 +288,7 @@ describe('Cloud Commands', () => {
expect(await cloud._checkForAssets([dirPath])).to.equal(undefined);
});

it('returns undefined if project.properties is missing', async () => {
it('returns asset path if project.properties is missing', async () => {
const { cloud } = stubForLogin(new CloudCommands(), stubs);
const dirPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid-no-proj-prop');
expect(await cloud._checkForAssets([dirPath])).to.equal(undefined);
Expand All @@ -298,7 +298,7 @@ describe('Cloud Commands', () => {
const { cloud } = stubForLogin(new CloudCommands(), stubs);
const dirPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid-no-proj-prop');
const projectPropertiesPath = path.join(dirPath, 'project.properties');
const projectPropertiesContent = 'project.name=valid-no-proj-prop\nassetOtaFolder=';
const projectPropertiesContent = 'project.name=valid-no-proj-prop\nassetOtaDir=';
await fs.writeFile(projectPropertiesPath, projectPropertiesContent);

expect(await cloud._checkForAssets([dirPath])).to.equal(undefined);
Expand All @@ -310,10 +310,10 @@ describe('Cloud Commands', () => {
const { cloud } = stubForLogin(new CloudCommands(), stubs);
const dirPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'valid-no-proj-prop');
const projectPropertiesPath = path.join(dirPath, 'project.properties');
const projectPropertiesContent = 'project.name=valid-no-proj-prop\nassetOtaFolder=foo';
const projectPropertiesContent = 'project.name=valid-no-proj-prop\nassetOtaDir=foo';
await fs.writeFile(projectPropertiesPath, projectPropertiesContent);

expect(await cloud._checkForAssets([dirPath])).to.equal(undefined);
expect(await cloud._checkForAssets([dirPath])).to.equal(path.join(dirPath, 'foo'));

await fs.unlink(projectPropertiesPath);
});
Expand Down
7 changes: 7 additions & 0 deletions src/lib/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ License along with this program; if not, see <http://www.gnu.org/licenses/>.

const fs = require('fs');
const _ = require('lodash');
const propertiesParser = require('properties-parser');
const os = require('os');
const path = require('path');
const glob = require('glob');
Expand Down Expand Up @@ -382,6 +383,12 @@ module.exports = {
return new Error(_.isArray(err) ? err.join('\n') : err);
}
return err;
},

parsePropertyFile(propPath) {
const savedProp = fs.readFileSync(propPath, 'utf8');
const parsedPropFile = propertiesParser.parse(savedProp);
return parsedPropFile;
}
};

Loading

0 comments on commit 328a903

Please sign in to comment.