mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-25 00:11:04 +00:00
222 lines
6.7 KiB
JavaScript
222 lines
6.7 KiB
JavaScript
|
/**
|
||
|
* @license
|
||
|
* Copyright 2016-2020 Balena Ltd.
|
||
|
*
|
||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
* you may not use this file except in compliance with the License.
|
||
|
* You may obtain a copy of the License at
|
||
|
*
|
||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
*
|
||
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
* See the License for the specific language governing permissions and
|
||
|
* limitations under the License.
|
||
|
*/
|
||
|
|
||
|
// Imported here because it's needed for the setup
|
||
|
// of this action
|
||
|
import * as Promise from 'bluebird';
|
||
|
|
||
|
import * as dockerUtils from '../utils/docker';
|
||
|
import * as compose from '../utils/compose';
|
||
|
import { registrySecretsHelp } from '../utils/messages';
|
||
|
import { getBalenaSdk } from '../utils/lazy';
|
||
|
|
||
|
/*
|
||
|
Opts must be an object with the following keys:
|
||
|
|
||
|
app: the app this build is for (optional)
|
||
|
arch: the architecture to build for
|
||
|
deviceType: the device type to build for
|
||
|
buildEmulated
|
||
|
buildOpts: arguments to forward to docker build command
|
||
|
*/
|
||
|
const buildProject = function(docker, logger, composeOpts, opts) {
|
||
|
const { loadProject } = require('../utils/compose_ts');
|
||
|
return Promise.resolve(loadProject(logger, composeOpts))
|
||
|
.then(function(project) {
|
||
|
const appType = opts.app?.application_type?.[0];
|
||
|
if (
|
||
|
appType != null &&
|
||
|
project.descriptors.length > 1 &&
|
||
|
!appType.supports_multicontainer
|
||
|
) {
|
||
|
logger.logWarn(
|
||
|
'Target application does not support multiple containers.\n' +
|
||
|
'Continuing with build, but you will not be able to deploy.',
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return compose.buildProject(
|
||
|
docker,
|
||
|
logger,
|
||
|
project.path,
|
||
|
project.name,
|
||
|
project.composition,
|
||
|
opts.arch,
|
||
|
opts.deviceType,
|
||
|
opts.buildEmulated,
|
||
|
opts.buildOpts,
|
||
|
composeOpts.inlineLogs,
|
||
|
opts.convertEol,
|
||
|
composeOpts.dockerfilePath,
|
||
|
);
|
||
|
})
|
||
|
.then(function() {
|
||
|
logger.outputDeferredMessages();
|
||
|
logger.logSuccess('Build succeeded!');
|
||
|
})
|
||
|
.tapCatch(() => {
|
||
|
logger.logError('Build failed');
|
||
|
});
|
||
|
};
|
||
|
|
||
|
export const build = {
|
||
|
signature: 'build [source]',
|
||
|
description: 'Build a single image or a multicontainer project locally',
|
||
|
primary: true,
|
||
|
help: `\
|
||
|
Use this command to build an image or a complete multicontainer project with
|
||
|
the provided docker daemon in your development machine or balena device.
|
||
|
(See also the \`balena push\` command for the option of building images in the
|
||
|
balenaCloud build servers.)
|
||
|
|
||
|
You must provide either an application or a device-type/architecture pair to use
|
||
|
the balena Dockerfile pre-processor (e.g. Dockerfile.template -> Dockerfile).
|
||
|
|
||
|
This command will look into the given source directory (or the current working
|
||
|
directory if one isn't specified) for a docker-compose.yml file, and if found,
|
||
|
each service defined in the compose file will be built. If a compose file isn't
|
||
|
found, it will look for a Dockerfile[.template] file (or alternative Dockerfile
|
||
|
specified with the \`--dockerfile\` option), and if no dockerfile is found, it
|
||
|
will try to generate one.
|
||
|
|
||
|
${registrySecretsHelp}
|
||
|
|
||
|
Examples:
|
||
|
|
||
|
$ balena build
|
||
|
$ balena build ./source/
|
||
|
$ balena build --deviceType raspberrypi3 --arch armv7hf --emulated
|
||
|
$ balena build --application MyApp ./source/
|
||
|
$ balena build --docker /var/run/docker.sock # Linux, Mac
|
||
|
$ balena build --docker //./pipe/docker_engine # Windows
|
||
|
$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem\
|
||
|
`,
|
||
|
options: dockerUtils.appendOptions(
|
||
|
compose.appendOptions([
|
||
|
{
|
||
|
signature: 'arch',
|
||
|
parameter: 'arch',
|
||
|
description: 'The architecture to build for',
|
||
|
alias: 'A',
|
||
|
},
|
||
|
{
|
||
|
signature: 'deviceType',
|
||
|
parameter: 'deviceType',
|
||
|
description: 'The type of device this build is for',
|
||
|
alias: 'd',
|
||
|
},
|
||
|
{
|
||
|
signature: 'application',
|
||
|
parameter: 'application',
|
||
|
description: 'The target balena application this build is for',
|
||
|
alias: 'a',
|
||
|
},
|
||
|
]),
|
||
|
),
|
||
|
action(params, options) {
|
||
|
// compositions with many services trigger misleading warnings
|
||
|
// @ts-ignore editing property that isn't typed but does exist
|
||
|
require('events').defaultMaxListeners = 1000;
|
||
|
|
||
|
const sdk = getBalenaSdk();
|
||
|
const { ExpectedError } = require('../errors');
|
||
|
const { checkLoggedIn } = require('../utils/patterns');
|
||
|
const { validateProjectDirectory } = require('../utils/compose_ts');
|
||
|
const helpers = require('../utils/helpers');
|
||
|
const Logger = require('../utils/logger');
|
||
|
|
||
|
const logger = Logger.getLogger();
|
||
|
logger.logDebug('Parsing input...');
|
||
|
|
||
|
// `build` accepts `[source]` as a parameter, but compose expects it
|
||
|
// as an option. swap them here
|
||
|
if (options.source == null) {
|
||
|
options.source = params.source;
|
||
|
}
|
||
|
delete params.source;
|
||
|
|
||
|
options.convertEol = options['convert-eol'] || false;
|
||
|
delete options['convert-eol'];
|
||
|
|
||
|
const { application, arch, deviceType } = options;
|
||
|
|
||
|
return Promise.try(function() {
|
||
|
if (
|
||
|
(application == null && (arch == null || deviceType == null)) ||
|
||
|
(application != null && (arch != null || deviceType != null))
|
||
|
) {
|
||
|
throw new ExpectedError(
|
||
|
'You must specify either an application or an arch/deviceType pair to build for',
|
||
|
);
|
||
|
}
|
||
|
if (application) {
|
||
|
return checkLoggedIn();
|
||
|
}
|
||
|
})
|
||
|
.then(() =>
|
||
|
validateProjectDirectory(sdk, {
|
||
|
dockerfilePath: options.dockerfile,
|
||
|
noParentCheck: options['noparent-check'] || false,
|
||
|
projectPath: options.source || '.',
|
||
|
registrySecretsPath: options['registry-secrets'],
|
||
|
}),
|
||
|
)
|
||
|
.then(function({ dockerfilePath, registrySecrets }) {
|
||
|
options.dockerfile = dockerfilePath;
|
||
|
options['registry-secrets'] = registrySecrets;
|
||
|
|
||
|
if (arch != null && deviceType != null) {
|
||
|
return [undefined, arch, deviceType];
|
||
|
} else {
|
||
|
return Promise.join(
|
||
|
helpers.getApplication(application),
|
||
|
helpers.getArchAndDeviceType(application),
|
||
|
function(
|
||
|
app,
|
||
|
{ arch: resolvedArch, device_type: resolvedDeviceType },
|
||
|
) {
|
||
|
// @ts-ignore extending the app object
|
||
|
app.arch = resolvedArch;
|
||
|
app.device_type = resolvedDeviceType;
|
||
|
return app;
|
||
|
},
|
||
|
).then(app =>
|
||
|
// @ts-ignore using the extended prop
|
||
|
[app, app.arch, app.device_type],
|
||
|
);
|
||
|
}
|
||
|
})
|
||
|
|
||
|
.then(function([app, resolvedArch, resolvedDeviceType]) {
|
||
|
return Promise.join(
|
||
|
dockerUtils.getDocker(options),
|
||
|
dockerUtils.generateBuildOpts(options),
|
||
|
compose.generateOpts(options),
|
||
|
(docker, buildOpts, composeOpts) =>
|
||
|
buildProject(docker, logger, composeOpts, {
|
||
|
app,
|
||
|
arch: resolvedArch,
|
||
|
deviceType: resolvedDeviceType,
|
||
|
buildEmulated: !!options.emulated,
|
||
|
buildOpts,
|
||
|
convertEol: options.convertEol,
|
||
|
}),
|
||
|
);
|
||
|
});
|
||
|
},
|
||
|
};
|