Akos Kitta 9ef04bb8d6 Fixed missing translations
Aligned the languge pack versions.

Closes #1431

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-26 10:49:31 +02:00

526 lines
19 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//@ts-check
(async () => {
const fs = require('fs');
const join = require('path').join;
const shell = require('shelljs');
const glob = require('glob');
const isCI = require('is-ci');
shell.env.THEIA_ELECTRON_SKIP_REPLACE_FFMPEG = '1'; // Do not run the ffmpeg validation for the packager.
// Note, this will crash on PI if the available memory is less than desired heap size.
// https://github.com/shelljs/shelljs/issues/1024#issuecomment-1001552543
shell.env.NODE_OPTIONS = '--max_old_space_size=4096'; // Increase heap size for the CI
shell.env.PUPPETEER_SKIP_CHROMIUM_DOWNLOAD = 'true'; // Skip download and avoid `ERROR: Failed to download Chromium`.
const template = require('./config').generateTemplate(
new Date().toISOString()
);
const utils = require('./utils');
const merge = require('deepmerge');
const { isRelease, getChannelFile } = utils;
const { version } = template;
const { productName } = template.build;
echo(`📦 Building ${isRelease ? 'release ' : ''}version '${version}'...`);
const workingCopy = 'working-copy';
/**
* Relative path from the `__dirname` to the root where the `arduino-ide-extension` and the `electron-app` folders are.
* This could come handy when moving the location of the `electron/packager`.
*/
const rootPath = join('..', '..');
// This is a HACK! We rename the root `node_modules` to something else. Otherwise, due to the hoisting,
// multiple Theia extensions will be picked up.
if (fs.existsSync(path(rootPath, 'node_modules'))) {
// We either do this or change the project structure.
echo(
"🔧 >>> [Hack] Renaming the root 'node_modules' folder to '.node_modules'..."
);
mv('-f', path(rootPath, 'node_modules'), path(rootPath, '.node_modules'));
echo(
"👌 <<< [Hack] Renamed the root 'node_modules' folder to '.node_modules'."
);
}
//---------------------------+
// Clean the previous state. |
//---------------------------+
// rm -rf ../working-copy
rm('-rf', path('..', workingCopy));
// Clean up the `./electron/build` folder.
const resourcesToKeep = [
'patch',
'resources',
'scripts',
'template-package.json',
'webpack.config.js'
];
fs.readdirSync(path('..', 'build'))
.filter((filename) => resourcesToKeep.indexOf(filename) === -1)
.forEach((filename) => rm('-rf', path('..', 'build', filename)));
// Clean up the `./electron/build/patch` and `./electron/build/resources` folder with Git.
// To avoid file duplication between bundled app and dev mode, some files are copied from `./electron-app` to `./electron/build` folder.
const foldersToSyncFromDev = ['patch', 'resources'];
foldersToSyncFromDev.forEach(filename => shell.exec(`git -C ${path('..', 'build', filename)} clean -ffxdq`, { async: false }));
const extensions = require('./extensions.json');
echo(
`Building the application with the following extensions:\n${extensions
.map((ext) => ` - ${ext}`)
.join(',\n')}`
);
const allDependencies = [...extensions, 'electron-app'];
//----------------------------------------------------------------------------------------------+
// Copy the following items into the `working-copy` folder. Make sure to reuse the `yarn.lock`. |
//----------------------------------------------------------------------------------------------+
mkdir('-p', path('..', workingCopy));
for (const filename of [
...allDependencies,
'yarn.lock',
'package.json',
'lerna.json',
'i18n'
]) {
cp('-rf', path(rootPath, filename), path('..', workingCopy));
}
//---------------------------------------------------------------------------------------------+
// Copy the patched `index.js` for the frontend, the Theia preload, etc. from `./electron-app` |
//---------------------------------------------------------------------------------------------+
for (const filename of foldersToSyncFromDev) {
cp('-rf', path('..', workingCopy, 'electron-app', filename), path('..', 'build'));
}
//----------------------------------------------+
// Sanity check: all versions must be the same. |
//----------------------------------------------+
verifyVersions(allDependencies);
//----------------------------------------------------------------------+
// Use the nightly patch version if not a release but requires publish. |
//----------------------------------------------------------------------+
if (!isRelease) {
for (const dependency of allDependencies) {
const pkg = require(`../working-copy/${dependency}/package.json`);
pkg.version = version;
for (const dependency in pkg.dependencies) {
if (allDependencies.indexOf(dependency) !== -1) {
pkg.dependencies[dependency] = version;
}
}
fs.writeFileSync(
path('..', workingCopy, dependency, 'package.json'),
JSON.stringify(pkg, null, 2)
);
}
}
verifyVersions(allDependencies);
//---------------------------------------------------------------------------------------------------+
// Save some time: no need to build the projects that are not needed in final app. Currently unused. |
//---------------------------------------------------------------------------------------------------+
//@ts-ignore
const rootPackageJson = require('../working-copy/package.json');
const workspaces = rootPackageJson.workspaces;
// We cannot remove the `electron-app`. Otherwise, there is not way to collect the unused dependencies.
const dependenciesToRemove = [];
for (const dependencyToRemove of dependenciesToRemove) {
const index = workspaces.indexOf(dependencyToRemove);
if (index !== -1) {
workspaces.splice(index, 1);
}
}
rootPackageJson.workspaces = workspaces;
fs.writeFileSync(
path('..', workingCopy, 'package.json'),
JSON.stringify(rootPackageJson, null, 2)
);
//-------------------------------------------------------------------------------------------------+
// Rebuild the extension with the copied `yarn.lock`. It is a must to use the same Theia versions. |
//-------------------------------------------------------------------------------------------------+
exec(
`yarn --network-timeout 1000000 --cwd ${path('..', workingCopy)}`,
`Building the ${productName} application`
);
//-------------------------------------------------------------------------------------------------------------------------+
// Test the application. With this approach, we cannot publish test results to GH Actions but save 6-10 minutes per builds |
//-------------------------------------------------------------------------------------------------------------------------+
exec(
`yarn --network-timeout 1000000 --cwd ${path('..', workingCopy)} test`,
`Testing the ${productName} application`
);
// Collect all unused dependencies by the backend. We have to remove them from the electron app.
// The `bundle.js` already contains everything we need for the frontend.
// We have to do it before changing the dependencies to `local-path`.
const unusedDependencies = await utils.collectUnusedDependencies(
'../working-copy/electron-app/'
);
//-------------------------------------------------------------------------------------------------------------+
// Change the regular NPM dependencies to `local-paths`, so that we can build them without any NPM registries. |
//-------------------------------------------------------------------------------------------------------------+
for (const extension of extensions) {
if (extension !== 'arduino-ide-extension') {
// Do not unlink self.
// @ts-ignore
rootPackageJson = require(`../working-copy/${extension}/package.json`);
// @ts-ignore
rootPackageJson.dependencies['arduino-ide-extension'] =
'file:../arduino-ide-extension';
fs.writeFileSync(
path('..', workingCopy, extension, 'package.json'),
JSON.stringify(rootPackageJson, null, 2)
);
}
}
//------------------------------------------------------------------------------------+
// Merge the `working-copy/package.json` with `electron/build/template-package.json`. |
//------------------------------------------------------------------------------------+
// @ts-ignore
const appPackageJson = require('../working-copy/electron-app/package.json');
template.build.files = [
...template.build.files,
...unusedDependencies.map((name) => `!node_modules/${name}`),
];
const dependencies = {};
for (const extension of extensions) {
dependencies[extension] = `file:../working-copy/${extension}`;
}
// @ts-ignore
appPackageJson.dependencies = { ...appPackageJson.dependencies, ...dependencies };
appPackageJson.devDependencies = { ...appPackageJson.devDependencies, ...template.devDependencies };
// Deep-merging the Theia application configuration.
// @ts-ignore
const theia = merge(appPackageJson.theia || {}, template.theia || {});
const content = {
...appPackageJson,
...template,
theia,
// @ts-ignore
dependencies: appPackageJson.dependencies,
devDependencies: appPackageJson.devDependencies,
// VS Code extensions and the plugins folder is defined in the top level `package.json`. The template picks them up.
theiaPluginsDir: rootPackageJson.theiaPluginsDir,
theiaPlugins: rootPackageJson.theiaPlugins,
};
fs.writeFileSync(
path('..', 'build', 'package.json'),
JSON.stringify(
merge(content, template, { arrayMerge: (_, sourceArray) => sourceArray }),
null,
2
)
);
echo(`📜 Effective 'package.json' for the ${productName} application is:
-----------------------
${fs.readFileSync(path('..', 'build', 'package.json')).toString()}
-----------------------
`);
// Make sure the original `yarn.lock` file is used from the electron application.
if (fs.existsSync(path('..', 'build', 'yarn.lock'))) {
echo(`${path('..', 'build', 'yarn.lock')} must not exist.`);
shell.exit(1);
}
cp('-rf', path(rootPath, 'yarn.lock'), path('..', 'build'));
if (!fs.existsSync(path('..', 'build', 'yarn.lock'))) {
echo(`${path('..', 'build', 'yarn.lock')} does not exist.`);
shell.exit(1);
}
//-------------------------------------------------------------------------------------------+
// Install all private and public dependencies for the electron application and build Theia. |
//-------------------------------------------------------------------------------------------+
exec(
`yarn --network-timeout 1000000 --cwd ${path('..', 'build')}`,
'Installing dependencies'
);
exec(
`yarn --network-timeout 1000000 --cwd ${path('..', 'build')} build`,
`Building the ${productName} application`
);
exec(
`yarn --network-timeout 1000000 --cwd ${path('..', 'build')} rebuild`,
'Rebuild native dependencies'
);
//------------------------------------------------------------------------------+
// Create a throw away dotenv file which we use to feed the builder with input. |
//------------------------------------------------------------------------------+
const dotenv = 'electron-builder.env';
if (fs.existsSync(path('..', 'build', dotenv))) {
rm('-rf', path('..', 'build', dotenv));
}
// For the releases we use the desired tag as is defined by `$(Release.Tag)` from Azure.
// For the preview builds we use the version from the `electron/build/package.json` with the short commit hash.
fs.writeFileSync(path('..', 'build', dotenv), `ARDUINO_VERSION=${version}`);
//-----------------------------------+
// Package the electron application. |
//-----------------------------------+
exec(
`yarn --network-timeout 1000000 --cwd ${path('..', 'build')} package`,
`Packaging your ${productName} application`
);
//-----------------------------------------------------------------------------------------------------+
// Recalculate artifacts hash and copy to another folder (because they can change after signing them).
// Azure does not support wildcard for `PublishBuildArtifacts@1.pathToPublish` |
//-----------------------------------------------------------------------------------------------------+
if (isCI) {
try {
await recalculateArtifactsHash();
await copyFilesToBuildArtifacts();
} catch (e) {
echo(JSON.stringify(e));
shell.exit(1);
}
}
echo(`🎉 Success. Your application is at: ${path('..', 'build', 'dist')}`);
restore();
//--------+
// Utils. |
//--------+
function exec(command, toEcho) {
if (toEcho) {
echo(`⏱️ >>> ${toEcho}...`);
}
const { code, stderr, stdout } = shell.exec(command);
if (code !== 0) {
echo(`🔥 Error when executing ${command} => ${stderr}`);
shell.exit(1);
}
if (toEcho) {
echo(`👌 <<< ${toEcho}.`);
}
return stdout;
}
function cp(options, source, destination) {
shell.cp(options, source, destination);
assertNoError();
}
function rm(options, ...files) {
shell.rm(options, files);
assertNoError();
}
function mv(options, source, destination) {
shell.mv(options, source, destination);
assertNoError();
}
function mkdir(options, ...dir) {
shell.mkdir(options, dir);
assertNoError();
}
function echo(command) {
return shell.echo(command);
}
function assertNoError() {
const error = shell.error();
if (error) {
echo(error);
restore();
shell.exit(1);
}
}
function restore() {
if (fs.existsSync(path(rootPath, '.node_modules'))) {
echo(
"🔧 >>> [Restore] Renaming the root '.node_modules' folder to 'node_modules'..."
);
mv('-f', path(rootPath, '.node_modules'), path(rootPath, 'node_modules'));
echo(
"👌 >>> [Restore] Renamed the root '.node_modules' folder to 'node_modules'."
);
}
}
async function copyFilesToBuildArtifacts() {
echo(`🚢 Detected CI, moving build artifacts...`);
const { platform } = process;
const cwd = path('..', 'build', 'dist');
const targetFolder = path('..', 'build', 'dist', 'build-artifacts');
mkdir('-p', targetFolder);
const filesToCopy = [];
const channelFile = getChannelFile(platform);
// Channel file might be an empty string if we're not building a
// nightly or a full release. This can happen when building a package
// locally or a tester build when creating a new PR on GH.
if (!!channelFile && fs.existsSync(join(cwd, channelFile))) {
const channelFilePath = join(cwd, channelFile);
const newChannelFilePath = channelFilePath
?.replace('latest', 'stable')
?.replace('beta', 'nightly');
echo(`🔨 >>> Renaming ${channelFilePath} to ${newChannelFilePath}.`);
cp('-f', channelFilePath, newChannelFilePath);
filesToCopy.push(newChannelFilePath);
}
switch (platform) {
case 'linux': {
filesToCopy.push(
...glob
.sync('**/arduino-ide*.{zip,AppImage}', { cwd })
.map((p) => join(cwd, p))
);
break;
}
case 'win32': {
filesToCopy.push(
...glob
.sync('**/arduino-ide*.{exe,msi,zip}', { cwd })
.map((p) => join(cwd, p))
);
break;
}
case 'darwin': {
filesToCopy.push(
...glob
.sync('**/arduino-ide*.{dmg,zip}', { cwd })
.map((p) => join(cwd, p))
);
break;
}
default: {
echo(`Unsupported platform: ${platform}.`);
shell.exit(1);
}
}
if (!filesToCopy.length) {
echo(`Could not collect any build artifacts from ${cwd}.`);
shell.exit(1);
}
for (const fileToCopy of filesToCopy) {
echo(`🚢 >>> Copying ${fileToCopy} to ${targetFolder}.`);
const isZip = await utils.isZip(fileToCopy);
if (isZip && platform === 'linux') {
await utils.adjustArchiveStructure(fileToCopy, targetFolder);
} else {
cp('-rf', fileToCopy, targetFolder);
}
echo(`👌 >>> Copied ${fileToCopy} to ${targetFolder}.`);
}
}
async function recalculateArtifactsHash() {
echo(`🚢 Detected CI, recalculating artifacts hash...`);
const { platform } = process;
const cwd = path('..', 'build', 'dist');
const channelFilePath = join(cwd, getChannelFile(platform));
const yaml = require('yaml');
try {
let fileContents = fs.readFileSync(channelFilePath, 'utf8');
const newChannelFile = yaml.parse(fileContents);
const { files, path } = newChannelFile;
const newSha512 = await hashFile(join(cwd, path));
newChannelFile.sha512 = newSha512;
if (!!files) {
const newFiles = [];
for (let file of files) {
const { url } = file;
const { size } = fs.statSync(join(cwd, url));
const newSha512 = await hashFile(join(cwd, url));
if (!newFiles.find((f) => f.sha512 === newSha512)) {
newFiles.push({ ...file, sha512: newSha512, size });
}
}
newChannelFile.files = newFiles;
}
const newChannelFileRaw = yaml.stringify(newChannelFile);
fs.writeFileSync(channelFilePath, newChannelFileRaw);
echo(`👌 >>> Channel file updated successfully. New channel file:`);
echo(newChannelFileRaw);
} catch (e) {
console.log(e);
}
}
/**
* @param {import('fs').PathLike} file
* @param {string|undefined} [algorithm="sha512"]
* @param {BufferEncoding|undefined} [encoding="base64"]
* @param {object|undefined} [options]
*/
async function hashFile(
file,
algorithm = 'sha512',
encoding = 'base64',
options
) {
const crypto = require('crypto');
return await new Promise((resolve, reject) => {
const hash = crypto.createHash(algorithm);
hash.on('error', reject).setEncoding(encoding);
fs.createReadStream(
file,
Object.assign({}, options, {
highWaterMark: 1024 * 1024,
/* better to use more memory but hash faster */
})
)
.on('error', reject)
.on('end', () => {
hash.end();
resolve(hash.read());
})
.pipe(hash, {
end: false,
});
});
}
/**
* Joins tha path from `__dirname`.
*/
function path(...paths) {
return join(__dirname, ...paths);
}
function verifyVersions(allDependencies, expectedVersion) {
const versions = new Set();
for (const dependency of allDependencies) {
versions.add(
require(`../working-copy/${dependency}/package.json`).version
);
}
if (versions.size !== 1) {
echo(
`Mismatching version configuration. All dependencies must have the same version. Versions were: ${JSON.stringify(
Array.from(versions),
null,
2
)}.`
);
shell.exit(1);
}
if (expectedVersion) {
if (!versions.has(expectedVersion)) {
echo(
`Mismatching version configuration. Expected version was: '${expectedVersion}' actual was: '${Array.from(versions)[0]
}'.`
);
shell.exit(1);
}
}
}
})();