@@ -1,9 +1,31 @@
//@ts-check
( async ( ) => {
const toDispose = [ ] ;
const disposeAll = ( ) => {
let disposable = toDispose . pop ( ) ;
while ( disposable ) {
try {
disposable ( ) ;
} catch ( err ) {
console . error ( err ) ;
}
disposable = toDispose . pop ( ) ;
}
} ;
process . on ( 'uncaughtException' , ( error ) => {
disposeAll ( ) ;
throw error ;
} ) ;
process . on ( 'unhandledRejection' , ( reason ) => {
disposeAll ( ) ;
throw reason ;
} ) ;
const fs = require ( 'fs' ) ;
const join = require ( 'path' ) . join ;
const shell = require ( 'shelljs' ) ;
const { echo , cp , mkdir , mv , rm } = shell ;
shell . config . fatal = true ;
const glob = require ( 'glob' ) ;
const isCI = require ( 'is-ci' ) ;
// Note, this will crash on PI if the available memory is less than desired heap size.
@@ -21,175 +43,124 @@
echo ( ` 📦 Building ${ isRelease ? 'release ' : '' } version ' ${ version } '... ` ) ;
const workingCopy = 'working-copy' ;
const repoRoot = join ( _ _dirname , '..' , '..' ) ;
/**
* 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`.
* Extensions are expected to be folders directly available from the repository root .
*/
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'."
) ;
}
try {
//---------------------------+
// 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'
] ;
fs . readdirSync ( path ( '..' , 'build' ) )
. filter ( ( filename ) => resourcesToKeep . indexOf ( filename ) === - 1 )
. forEach ( ( filename ) => rm ( '-rf' , path ( '..' , 'build' , filename ) ) ) ;
// Clean up the `./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 = [ '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 ' ,
'i18 n' ,
] ) {
cp ( '-rf' , path ( rootPath , filename ) , path ( '..' , workingCopy ) ) ;
try {
//---------------------------+
// Clean the previous state. |
//---------------------------+
// Clean up the `./electron/build` folder.
const resourcesToKeep = [
'patch ' ,
'resources ' ,
'scripts ' ,
'template-package.jso n' ,
] ;
fs . readdirSync ( join ( repoRoot , 'electron' , 'build' ) )
. filter ( ( filename ) => resourcesToKeep . indexOf ( filename ) === - 1 )
. forEach ( ( filename ) =>
rm ( '-rf' , join ( repoRoot , 'electron' , 'build' , filename ) )
) ;
// Clean up the `./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 = [ 'resources' ] ;
foldersToSyncFromDev . forEach ( ( filename ) =>
shell . exec (
` git -C ${ join ( repoRoot , 'electron' , 'build' , filename ) } clean -ffxdq ` ,
{
async : false ,
}
)
) ;
//--------------------------------------------------------------------------------------------- +
// Copy the patched `index.js` for the frontend, the Theia preload, etc. from `./electron-app` |
//--------------------------------------------------------------------------------------------- +
//----------------------------------------------------+
// Copy the Theia preload, etc. from `./electron-app` |
//----------------------------------------------------+
for ( const filename of foldersToSyncFromDev ) {
cp (
'-rf' ,
path ( '..' , workingCopy , 'electron-app' , filename ) ,
path ( '.. ', 'build' )
join ( repoRoot , 'electron-app' , filename ) ,
join ( repoRoot , 'electron ', 'build' )
) ;
}
//----------------------------------------------+
// Sanity check: all versions must be the same. |
//----------------------------------------------+
verifyVersions ( allDependencie s) ;
verifyVersions ( extension s) ;
//---------------------------------------------------------------------- +
// 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 `
) ;
//-------------------------------------------------------------------------------------------------------------+
// Change the regular NPM dependencies to `local-paths`, so that we can build them without any NPM registries. |
//-------------------------------------------------------------------------------------------------------------+
//-------------------------------+
// Build and test the extensions |
//-------------------------------+
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 )
exec (
` yarn --network-timeout 1000000 --cwd ${ join ( repoRoot , extension ) } ` ,
` Building and testing ${ extension } `
) ;
}
//------------------------+
// Publish the extensions |
//------------------------+
const npmrc = join ( repoRoot , '.npmrc' ) ;
const storage = join ( _ _dirname , 'npm-registry' , 'storage' ) ;
rm ( '-rf' , npmrc ) ;
rm ( '-rf' , storage ) ;
// To avoid interactive npm login on the CI when publishing to the private registry.
// The actual token is fake and does not matter as the publishing is `$anonymous` anyway.
fs . writeFileSync ( npmrc , '//localhost:4873/:_authToken=placeholder\n' , {
encoding : 'utf8' ,
} ) ;
toDispose . push ( ( ) => rm ( '-rf' , storage ) ) ;
toDispose . push ( ( ) => rm ( '-rf' , npmrc ) ) ;
const npmProxyProcess = await startNpmRegistry (
join ( _ _dirname , 'npm-registry' , 'config.yml' )
) ;
toDispose . push ( ( ) => {
if ( ! npmProxyProcess . killed ) {
npmProxyProcess . kill ( ) ;
}
} ) ;
for ( const extension of extensions ) {
const packageJsonPath = join ( repoRoot , extension , 'package.json' ) ;
const versionToRestore = readJson ( packageJsonPath ) . version ;
exec (
` yarn --network-timeout 1000000 --cwd ${ join (
repoRoot ,
extension
) } publish --ignore-scripts --new-version ${ version } --no-git-tag-version --registry http://localhost:4873 ` ,
` Publishing ${ extension } @ ${ version } to the private npm registry `
) ;
// Publishing will change the version number, this should be reverted up after the build.
// A git checkout or reset could be easier, but this is safer to avoid wiping uncommitted dev state.
toDispose . push ( ( ) => {
const json = readJson ( packageJsonPath ) ;
json . version = versionToRestore ;
writeJson ( packageJsonPath , json ) ;
} ) ;
}
//------------------------------------------------------------------------------------+
// Merge the `working-copy /package.json` with `electron/build/template-package.json`. |
//------------------------------------------------------------------------------------+
// @ts-ignore
const appPackageJson = require ( '../working-copy/electron-app/package.j son' ) ;
//----------------------------------------------------------------------------------------------------------- +
// Merge the `./package.json` and `./electron-app /package.json` with `electron/build/template-package.json`. |
//----------------------------------------------------------------------------------------------------------- +
const rootPackageJson = readJson ( join ( repoRoot , 'package.json' ) ) ;
const appPackageJson = readJ son (
join ( repoRoot , 'electron-app' , 'package.json' )
) ;
const dependencies = { } ;
for ( const extension of extensions ) {
dependencies [ extension ] = ` file:../working-copy/ ${ extension } ` ;
dependencies [ extension ] = version ;
}
// @ts-ignore
appPackageJson . dependencies = {
... appPackageJson . dependencies ,
... dependencies ,
@@ -199,80 +170,115 @@
... 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.
// VS Code extensions and the plugins folder is defined in the root `package.json`. The template picks them up.
theiaPluginsDir : rootPackageJson . theiaPluginsDir ,
theiaPlugins : rootPackageJson . theiaPlugins ,
} ;
fs . writeFileSync (
path ( '.. ', 'build' , 'package.json' ) ,
JSON . stringify (
writeJson (
join ( repoRoot , 'electron ', 'build' , 'package.json' ) ,
merge ( content , template , {
arrayMerge : ( _ , sourceArray ) => sourceArray ,
} ) ,
null ,
2
)
} )
) ;
echo ( ` 📜 Effective 'package.json' for the ${ productName } application is :
echo ( ` 📜 Effective 'package.json' for the ${ productName } application:
-----------------------
${ fs . readFileSync ( path ( '..' , 'build' , 'package.json' ) ) . toString ( ) }
${ fs
. readFileSync ( join ( repoRoot , 'electron' , '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. ` ) ;
if ( fs . existsSync ( join ( repoRoot , 'electron ', 'build' , 'yarn.lock' ) ) ) {
echo (
` ${ join ( repoRoot , 'electron' , '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. ` ) ;
cp ( '-rf' , join ( repoRoot , 'yarn.lock' ) , join ( repoRoot , 'electron ', 'build' ) ) ;
if ( ! fs . existsSync ( join ( repoRoot , 'electron ', 'build' , 'yarn.lock' ) ) ) {
echo (
` ${ join ( repoRoot , 'electron' , 'build' , 'yarn.lock' ) } does not exist. `
) ;
shell . exit ( 1 ) ;
}
// 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 ( join ( repoRoot , 'node_modules' ) ) ) {
// We either do this or change the project structure.
echo (
"🔧 >>> [Hack] Renaming the root 'node_modules' folder to '.node_modules'..."
) ;
mv ( '-f' , join ( repoRoot , 'node_modules' ) , join ( repoRoot , '.node_modules' ) ) ;
echo (
"👌 <<< [Hack] Renamed the root 'node_modules' folder to '.node_modules'."
) ;
}
toDispose . push ( ( ) => {
if ( fs . existsSync ( join ( repoRoot , '.node_modules' ) ) ) {
echo (
"🔧 >>> [Restore] Renaming the root '.node_modules' folder to 'node_modules'..."
) ;
mv (
'-f' ,
join ( repoRoot , '.node_modules' ) ,
join ( repoRoot , 'node_modules' )
) ;
echo (
"👌 >>> [Restore] Renamed the root '.node_modules' folder to 'node_modules'."
) ;
}
} ) ;
//-------------------------------------------------------------------------------------------+
// Install all private and public dependencies for the electron application and build Theia. |
//-------------------------------------------------------------------------------------------+
exec (
` yarn --network-timeout 1000000 --cwd ${ path ( '..' , 'build' ) } ` ,
` yarn --network-timeout 1000000 --cwd ${ join (
repoRoot ,
'electron' ,
'build'
) } --registry http://localhost:4873 ` ,
'Installing dependencies'
) ;
exec (
` yarn --network-timeout 1000000 --cwd ${ path ( '.. ' , 'build' ) } build ` ,
` yarn --cwd ${ join ( repoRoot , 'electron ' , 'build' ) } build ` ,
` Building the ${ productName } application `
) ;
exec (
` yarn --network-timeout 1000000 --cwd ${ path ( '.. ' , 'build' ) } rebuild ` ,
'Rebuild native dependencies'
` yarn --cwd ${ join ( repoRoot , 'electron ' , 'build' ) } rebuild ` ,
'Rebuilding 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 ) ) ;
if ( fs . existsSync ( join ( repoRoot , 'electron ', 'build' , dotenv ) ) ) {
rm ( '-rf' , join ( repoRoot , 'electron ', '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 } ` ) ;
fs . writeFileSync (
join ( repoRoot , 'electron' , 'build' , dotenv ) ,
` ARDUINO_VERSION= ${ version } `
) ;
//-----------------------------------+
// Package the electron application. |
//-----------------------------------+
exec (
` yarn --network-timeout 1000000 --cwd ${ path ( '.. ' , 'build' ) } package ` ,
` Packaging your ${ productName } application `
` yarn --cwd ${ join ( repoRoot , 'electron ' , 'build' ) } package ` ,
` Packaging the ${ productName } application `
) ;
//-----------------------------------------------------------------------------------------------------+
@@ -288,9 +294,16 @@ ${fs.readFileSync(path('..', 'build', 'package.json')).toString()}
shell . exit ( 1 ) ;
}
}
echo ( ` 🎉 Success. Your application is at: ${ path ( '..' , 'build' , 'dist' ) } ` ) ;
echo (
` 🎉 Success. The application is at: ${ join (
repoRoot ,
'electron' ,
'build' ,
'dist'
) } `
) ;
} finally {
restore ( ) ;
disposeAll ( ) ;
}
//--------+
@@ -300,67 +313,24 @@ ${fs.readFileSync(path('..', 'build', 'package.json')).toString()}
if ( toEcho ) {
echo ( ` ⏱️ >>> ${ toEcho } ... ` ) ;
}
const { code , stderr , stdout } = shell . exec ( command ) ;
if ( code !== 0 ) {
echo ( ` 🔥 Error when executing ${ command } => ${ stderr } ` ) ;
shell . exit ( 1 ) ;
}
const { stdout } = shell . exec ( command ) ;
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' ) ;
const cwd = join ( repoRoot , 'electron ', 'build' , 'dist' ) ;
const targetFolder = join (
repoRoot ,
'electron' ,
'build' ,
'dist' ,
'build-artifacts'
) ;
mkdir ( '-p' , targetFolder ) ;
const filesToCopy = [ ] ;
const channelFile = getChannelFile ( platform ) ;
@@ -425,7 +395,7 @@ ${fs.readFileSync(path('..', 'build', 'package.json')).toString()}
async function recalculateArtifactsHash ( ) {
echo ( ` 🚢 Detected CI, recalculating artifacts hash... ` ) ;
const { platform } = process ;
const cwd = path ( '.. ', 'build' , 'dist' ) ;
const cwd = join ( repoRoot , 'electron ', 'build' , 'dist' ) ;
const channelFilePath = join ( cwd , getChannelFile ( platform ) ) ;
const yaml = require ( 'yaml' ) ;
@@ -464,14 +434,14 @@ ${fs.readFileSync(path('..', 'build', 'package.json')).toString()}
* @param {BufferEncoding|undefined} [encoding="base64"]
* @param {object|undefined} [options]
*/
async function hashFile (
function hashFile (
file ,
algorithm = 'sha512' ,
encoding = 'base64' ,
options
) {
const crypto = require ( 'crypto' ) ;
return await new Promise ( ( resolve , reject ) => {
return new Promise ( ( resolve , reject ) => {
const hash = crypto . createHash ( algorithm ) ;
hash . on ( 'error' , reject ) . setEncoding ( encoding ) ;
fs . createReadStream (
@@ -493,17 +463,14 @@ ${fs.readFileSync(path('..', 'build', 'package.json')).toString()}
}
/**
* Joins tha path from `__dirname`.
* @param {string[]} allDependencies
* @param {string} [expectedVersion]
*/
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
readJson ( join ( repoRoot , dependency , ' package.json' ) ) . version
) ;
}
if ( versions . size !== 1 ) {
@@ -527,4 +494,41 @@ ${fs.readFileSync(path('..', 'build', 'package.json')).toString()}
}
}
}
/**
* @param {string} configPath
* @return {Promise<import('child_process').ChildProcess>}
*/
function startNpmRegistry ( configPath ) {
return new Promise ( ( resolve , reject ) => {
const fork = require ( 'child_process' ) . fork (
require . resolve ( 'verdaccio/bin/verdaccio' ) ,
[ '-c' , configPath ]
) ;
fork . on ( 'message' , ( msg ) => {
if ( typeof msg === 'object' && 'verdaccio_started' in msg ) {
resolve ( fork ) ;
}
} ) ;
fork . on ( 'error' , reject ) ;
fork . on ( 'disconnect' , reject ) ;
} ) ;
}
/**
* @param {string} path
* @param {object} jsonObject
*/
function writeJson ( path , jsonObject ) {
fs . writeFileSync ( path , JSON . stringify ( jsonObject , null , 2 ) + '\n' ) ;
}
/**
* @param {string} path
* @return {object}
*/
function readJson ( path ) {
const raw = fs . readFileSync ( path , { encoding : 'utf8' } ) ;
return JSON . parse ( raw ) ;
}
} ) ( ) ;