-
Notifications
You must be signed in to change notification settings - Fork 11
Creating Installers
The process of adding a new component is quite cumbersome and should be refactored into a much more maintainable structure. The best way to make sure you’re doing it right is to check installers that are already created in the repo and working currently, and base yours off of them.
Here are the steps to add a new installer.
Installers are created as JavaScript classes that respect the following contract:
- Contains a constructor.
- Contains an
async install()
method.
Here’s an example of an Installer that downloads a .zip and extracts it in a predefined directory.
// Require class dependencies.
const Modal = require( '../modal' );
const download = require( '../download' )();
const unzip = require( '../unzip' )();
/**
* Downloads and installs MyComponent.
* The class name must be the component’s name followed by "Installer". For example: SimitoneInstaller.
*/
class MyComponentInstaller {
/**
* Sets up the installer
*
* @param {string} path Final path to install in.
* @param {FSOLauncher} FSOLauncher
*/
constructor( path, FSOLauncher ) {
this.FSOLauncher = FSOLauncher;
// The path the user chose (coming from fsolauncher.js).
this.path = path;
// A "unique enough" identifier for this download.
this.id = Math.floor( Date.now() / 1000 );
// Variable that will break the progress loop.
this.haltProgress = false;
// The temporary directory for downloaded files.
this.tempPath = `${global.appData}temp/myzip-${this.id}.zip`; // Make sure to include the id to avoid possible collisions.
// Configure the download.
this.dl = download( { from: 'http://someurl.com/somefile.zip', to: this.tempPath } );
}
/**
* Creates or updates the progress bar.
*
* @param {string} message
* @param {number} percentage
*/
createProgressItem( message, percentage ) {
this.FSOLauncher.View.addProgressItem(
'FSOProgressItem' + this.id, // Include the id to avoid collisions.
'My Zipped File', // The download’s name.
'Installing in' + this.path, // The download’s info text. Remains static throughout the installation.
message, // The download’s progress text. More details about the current progress.
percentage // The download’s percentage, will be rendered in the progress bar.
);
// Set the native progress bar progress as well. (The one that appears in the taskbar icon, native to the OS)
this.FSOLauncher.setProgressBar( percentage == 100 ? 2 : percentage / 100 );
}
/**
* Executes all steps in order and returns a Promise.
*
* @return {Promise}
*/
async install() {
try {
await this.step1();
await this.step2();
await this.step3();
return this.end();
} catch( err ) {
return await this.error( err );
}
}
// Every step MUST return a Promise.
/**
* Downloads the zip into the temporary directory.
*
* @return {Promise}
*/
step1() { return this.download(); }
/**
* Makes sure the final directory exists.
*
* @return {Promise}
*/
step2() { return this.setupDir(); }
/**
* Extracts the zip into the final directory.
*
* @return {Promise}
*/
step3() { return this.extract(); }
/**
* Implement download(), setupDir() and extract().
* Base off of examples already in the repo if necessary.
* They MUST all return Promises.
*/
// ...
/**
* Cleans up and updates the progress to finished.
*/
end() {
// Do some cleanup. This is a method of the download() object.
this.dl.cleanup();
// Report the final progress (100%).
this.createProgressItem( 'Installation finished!', 100 );
// Mark the progress item as finished.
this.FSOLauncher.View.stopProgressItem( 'FSOProgressItem' + this.id );
// Update the installed programs list.
this.FSOLauncher.updateInstalledPrograms();
// Remove the task from the task array. Pass in the component’s name.
this.FSOLauncher.removeActiveTask( 'MyComponent' );
// Reset the native progress.
this.FSOLauncher.setProgressBar( -1 );
// Show a success Modal if it’s not a "Complete Installation", i.e. installing this component individually.
if( !this.isFullInstall ) Modal.showInstalled( 'MyComponent' );
}
/**
* Cleans up and updates the progress to error.
*
* @param {Error} err
*/
error( err ) {
// Do some cleanup. This is a method of the download() object.
this.dl.cleanup();
// Halt the progress manually. It might not have reached 100%.
this.haltProgress = true;
// Reset the native progress with error mode.
this.FSOLauncher.setProgressBar( 1, { mode: 'error' } );
// Show a short error message in the progress item.
this.createProgressItem( 'Failed to install MyComponent.', 100 );
// Mark the progress item as finished (errored out).
this.FSOLauncher.View.stopProgressItem( 'FSOProgressItem' + this.id );
// Remove the task from the task array. Pass in the component’s name.
this.FSOLauncher.removeActiveTask( 'Simitone' );
// Show a Modal telling the user the installation has failed, with the error message.
Modal.showFailedInstall( 'MyComponent', err );
// Return a Promise.reject, so that it throws an error.
return Promise.reject( err );
}
}
In src/fsolauncher/fsolauncher.js, add the Component Name and Code in getPrettyName()
. The code should be only a single word.
getPrettyName( Component ) {
switch ( Component ) {
case 'MCO': // Represents "MyCOmponent".
return "MyComponent";
// ... all the other components.
}
}
Modify fireInstallModal()
to add any dependencies (on other components) this component has.
switch ( Component ) {
case 'MCO':
// For our example, let’s say we require FreeSO to be installed.
if ( !this.isInstalled['FSO'] ) missing.push( this.getPrettyName( 'FSO' ) );
break;
}
If the component requires internet to be installed (you could include the binaries in the /bin/ folder, which would make the installation local-only),
you should add it to the array a bit below the switch, in the same method fireInstallModal()
.
if ( [ // Components that require internet access.
'TSO',
'FSO',
'RMS',
'Simitone',
'Mono',
'MacExtras',
'SDL',
'MCO', // Added MCO (a.k.a. MyComponent) here.
].indexOf( Component ) > -1 && !this.hasInternet ) {
return Modal.showNoInternet();
}
Modify src/fsolauncher/library/registry.js to include your component in the getInstalled()
method.
Use techniques already present in the class (registry/file existence check) to check if the component is installed.
static getInstalled() {
return new Promise( ( resolve, reject ) => {
const Promises = [];
Promises.push( Registry.get( 'OpenAL', Registry.getOpenALPath() ) );
Promises.push( Registry.get( 'FSO', Registry.getFSOPath() ) );
Promises.push( Registry.get( 'TSO', Registry.getTSOPath() ) );
Promises.push( Registry.get( 'NET', Registry.getNETPath() ) );
Promises.push( Registry.get( 'Simitone', Registry.getSimitonePath() ) );
Promises.push( Registry.get( 'TS1', Registry.getTS1Path() ) );
Promises.push( Registry.get( 'MCO', Registry.getMCOPath() ) ); // Added MCO. Make sure to implement this method getMCOPath.
if( process.platform == 'darwin' ) { // These are only for macOS.
Promises.push( Registry.get( 'Mono', Registry.getMonoPath() ) );
Promises.push( Registry.get( 'SDL', Registry.getSDLPath() ) );
}
Promise.all( Promises )
.then( a => resolve( a ) )
.catch( err => reject( err ) );
} );
}
To make this Installer able to be used, you need to modify the install()
method in src/fsolauncher/fsolauncher.js.
Add a switch case for the component that returns a Promise, and calls the installer’s install()
method.
case 'MCO': { // Switch case for MCO (a.k.a. MyComponent).
// Include our newly created installation class.
const MyComponentInstaller = require( './library/installers/mycomponent-installer' );
// Instantiate our installer.
const Installer = new MyComponentInstaller(
this.isInstalled.FSO + '/SomeFolderInsideFSO', this
);
// Navigate to the downloads page once this installation starts.
this.View.changePage( 'downloads' );
return new Promise( async ( resolve, reject ) => {
try {
await Installer.install();
resolve();
} catch ( e ) {
reject( e );
} finally {
// Make sure the native progress bar is reset.
setTimeout( () => this.setProgressBar( -1 ), 5000 );
}
} );
}
If the component is required to install FreeSO, add it to the complete installation flow. If it’s an optional component, just skip to step 7.
Edit the src/fsolauncher/library/installers/complete-installer.js file to add a new step that installs your new component.
The Complete Installer follows mostly the same structure as the installer we just created, but installs multiple components (using the FSOLauncher.install()
method) instead of just a single component.
The components in the installer screen have an image related to the component and some text.
You need to add a new component on this screen, so edit the src/fsolauncher_ui/fsolauncher.pug file.
Go to the section where the installer page is located at and add a new one.
- Notice the
install
attribute contains the component’s internal code. - Note also the inline styles which define the background image. You should add a backdrop image to the src/fsolauncher_ui/fsolauncher_images folder, and tweak it via these inline styles until it looks acceptable and the same as the other components.
- Finally, the
h1
andspan
contain the component’s name and a small tagline, respectively.
div
.item(install='MCO', style='background:url(fsolauncher_images/mco_backdrop.png) #fff; background-position:center center; background-size:80%;background-repeat:no-repeat;')
.tag
h1 #{INSTALLER_MCO_TITLE}
span #{INSTALLER_MCO_DESCR}
.tick.installed
i.material-icons done
Having followed these steps, users should be able to install the new component through the Complete Installer and individually, using the Installer page.
Home · User docs · Developer docs · FAQ