Skip to content

Creating Installers

Sim edited this page May 27, 2023 · 16 revisions

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 examples of the steps to add a new installer that install a new example component named "MyComponent", with "MCO" as its internal component code.

1. Create an installer class

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 {FSOLauncher} FSOLauncher
   * @param {string} path Final path to install in.
   */
  constructor( FSOLauncher, path ) {
    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 );
    // 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.IPC.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();
      this.end();
    } catch ( err ) {
      this.error( err );
      throw err; // Send it back to the caller.
    }
  }

  // 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.IPC.stopProgressItem( 'FSOProgressItem' + this.id );
  }

  /**
   * 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();

    // 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.IPC.stopProgressItem( 'FSOProgressItem' + this.id ); 
  }
}

module.exports = MyComponentInstaller;

2. Export the installer in src/fsolauncher/lib/installers/index.js

Use the component's code (short, singe-word, uppercase representation of the component's name) as the key. Use the same code in the next steps.

module.exports = {
  'FSO': require( './fso' ),
  'RMS': require( './rms' ),
  'TSO': require( './tso' ),
  'Simitone': require( './simitone' ),
  'SDL': require( './sdl' ),
  'Mono': require( './mono' ),
  'MacExtras': require( './macextras' ),
  'OpenAL': require( './executable' ),
  'NET': require( './executable' ),
  'MCO': require( './mco' ), // <-- The new component
};

3. Add the component name and code

In src/fsolauncher/constants.js, add the component's "pretty" name and code in components. The code should only be a single word.

  components: {
    'TSO': 'The Sims Online',
    'FSO': 'FreeSO',
    'OpenAL': 'OpenAL',
    'NET': '.NET Framework',
    'RMS': 'Remesh Package',
    'Simitone': 'Simitone for Windows',
    'Mono': 'Mono Runtime',
    'MacExtras': 'FreeSO MacExtras',
    'SDL': 'SDL2',
    'MCO' 'My Component' // <-- The new component
  }

3. Add dependencies, if any

In the same file, modify dependency to add any dependencies (on other components) this component has.

  dependency: {
    'FSO': [ 'TSO', ...( process.platform === 'darwin' ? [ 'Mono', 'SDL' ] : [ 'OpenAL' ] ) ],
    'RMS': [ 'FSO' ],
    'MacExtras': [ 'FSO' ],
    'Simitone': ( process.platform === 'darwin' ) ? [ 'Mono', 'SDL' ] : [],
    'MCO': [ 'FSO' ] // For our example, let’s say we require FreeSO to be installed.
  },

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 needInternet array, in the same file.

  needInternet: [
    'TSO',
    'FSO',
    'RMS',
    'Simitone',
    'Mono',
    'MacExtras',
    'SDL',
    'MCO' // <-- The new component
  ],

4. Make the Registry class aware of your component

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( resolve )
      .catch( reject );
  } );
}

5. Make the installer "installable"

To make this Installer able to be used, you need to modify the install() method in src/fsolauncher/fsolauncher.js.

Add the component's code to the switch in install():

  • handleSimpleInstall is for installs that are installed in the FreeSO directory. For example, remeshes that are extracted there. There is no way for the user to customize the installation.
  • handleStandardInstall is for installs that ask the user for a directory to install in - this directory is then passed into the installer class.
  • handleExecutableInstall is for running .exes present in the bin folder. Currently used for OpenAL.
      switch ( componentCode ) {
      case 'Mono':
      case 'MacExtras':
      case 'SDL':
      case 'RMS':
        display = await this.handleSimpleInstall( componentCode, options );
        break;
      case 'TSO':
      case 'FSO':
      case 'Simitone':
      case 'MCO': // Since the first parameter of the new installer's constructor is a path, we put it here - it will ask the user for a folder.
        display = await this.handleStandardInstall( componentCode, options );
        break;
      case 'OpenAL':
      case 'NET':
        display = await this.handleExecutableInstall( componentCode, options );
        break;

6. Add the component to the "Complete Installation" flow

If the component should be required to install FreeSO, add it to the complete installation flow. If it’s an optional component (like the RemeshPackage installer), 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.

7. Add the component to the Installer screen

The components in the installer screen have an image related to the component and some text.

The launcher’s installer page

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 and span 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
  • Make sure it looks good in Dark Mode as well! (Open Beta Dark and Halloween themes). Use the appropriate .css files in src/fsolauncher_ui/fsolauncher_styles/fsolauncher_themes to create overrides for when these themes are used.

Done

Having followed these steps, users should be able to install the new component through the Complete Installer and individually, using the Installer page.