From e931d8bf6cdf3431f8476abd6c2dd57876817026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Hern=C3=A1ndez?= Date: Wed, 9 Oct 2019 12:38:36 +0200 Subject: [PATCH 01/14] GPII-4171: Added Vagrantfile and scripts to set up the VM --- Vagrantfile | 34 ++++++++++++++++++ provisioning/Build.ps1 | 73 +++++++++++++++++++++++++++++++++++++++ provisioning/WixSetup.ps1 | 36 +++++++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 Vagrantfile create mode 100644 provisioning/Build.ps1 create mode 100644 provisioning/WixSetup.ps1 diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000..e81a930 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,34 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +Vagrant.require_version ">= 1.8.0" + +# By default this VM will use 2 processor cores and 2GB of RAM. The 'VM_CPUS' and +# "VM_RAM" environment variables can be used to change that behaviour. +cpus = ENV["VM_CPUS"] || 2 +ram = ENV["VM_RAM"] || 2048 + +Vagrant.configure(2) do |config| + + config.vm.box = "inclusivedesign/windows10-eval-x64-Apps" + config.vm.guest = :windows + + config.vm.communicator = "winrm" + config.winrm.username = "vagrant" + config.winrm.password = "vagrant" + config.vm.network :forwarded_port, guest: 3389, host: 3389, id: "rdp", auto_correct:true + config.vm.network :forwarded_port, guest: 5985, host: 5985, id: "rdp", auto_correct:true + + config.vm.provider :virtualbox do |vm| + vm.gui = true + vm.customize ["modifyvm", :id, "--memory", ram] + vm.customize ["modifyvm", :id, "--cpus", cpus] + vm.customize ["modifyvm", :id, "--vram", "256"] + vm.customize ["modifyvm", :id, "--accelerate3d", "off"] + vm.customize ["modifyvm", :id, "--audio", "null", "--audiocontroller", "hda"] + vm.customize ["modifyvm", :id, "--ioapic", "on"] + vm.customize ["setextradata", "global", "GUI/SuppressMessages", "all"] + end + + config.vm.provision "shell", path: "provisioning/Build.ps1", args: "-originalBuildScriptPath \"C:\\vagrant\\provisioning\\\"" +end diff --git a/provisioning/Build.ps1 b/provisioning/Build.ps1 new file mode 100644 index 0000000..47b44dc --- /dev/null +++ b/provisioning/Build.ps1 @@ -0,0 +1,73 @@ +<# + This script does the following: + 1) Run the provisioning scripts from the windows repository + 2) Run WixSetup.ps1 + 2) Run npm install + + If run via a tool (like vagrant) which moves this script to somewhere different + than its original location within the gpii-app repository, the parameter + "-originalBuildScriptPath" should be provided, with the original location of the + script +#> + +param ( + [string]$originalBuildScriptPath = (Split-Path -parent $PSCommandPath) # Default to script path. +) + +# Turn verbose on, change to "SilentlyContinue" for default behaviour. +$VerbosePreference = "continue" + +# Store the parent folder of the script (root of the repo) as $mainDir +############ +$mainDir = (get-item $originalBuildScriptPath).parent.FullName +Write-OutPut "mainDir set to: $($mainDir)" + +# TODO: We should add this to a function or reduce to oneline. +$bootstrapModule = Join-Path $originalBuildScriptPath "Provisioning.psm1" +iwr https://raw.githubusercontent.com/GPII/windows/master/provisioning/Provisioning.psm1 -UseBasicParsing -OutFile $bootstrapModule +Import-Module $bootstrapModule -Verbose -Force + +# Retrieve provisioning scripts from the windows repo +# ############ +# TODO: Create function for downloading scripts and executing them. +$windowsBootstrapURL = "https://raw.githubusercontent.com/GPII/windows/master/provisioning" +try { + $choco = Join-Path $originalBuildScriptPath "Chocolatey.ps1" + Write-OutPut "Running windows script: $choco" + iwr "$windowsBootstrapURL/Chocolatey.ps1" -UseBasicParsing -OutFile $choco + Invoke-Expression $choco +} catch { + Write-OutPut "Chocolatey.ps1 FAILED" + exit 1 +} +try { + $couchdb = Join-Path $originalBuildScriptPath "CouchDB.ps1" + Write-OutPut "Running windows script: $couchdb" + iwr "$windowsBootstrapURL/CouchDB.ps1" -UseBasicParsing -OutFile $couchdb + Invoke-Expression $couchdb +} catch { + Write-OutPut "CouchDB.ps1 FAILED" + exit 1 +} +try { + $npm = Join-Path $originalBuildScriptPath "Npm.ps1" + Write-OutPut "Running windows script: $npm" + iwr "$windowsBootstrapURL/Npm.ps1" -UseBasicParsing -OutFile $npm + Invoke-Expression $npm +} catch { + Write-OutPut "Npm.ps1 FAILED" + exit 1 +} + +## In addition to the previous scripts, we also need to setup Wix +try { + $wix = Join-Path $originalBuildScriptPath "WixSetup.ps1" + Write-OutPut "Setting up Wix: $wix" + Invoke-Expression $Wix +} catch { + Write-OutPut "WixSetup.ps1 FAILED" + exit 1 +} + +## Run npm install +Invoke-Command "npm" "install" $mainDir diff --git a/provisioning/WixSetup.ps1 b/provisioning/WixSetup.ps1 new file mode 100644 index 0000000..bb74d37 --- /dev/null +++ b/provisioning/WixSetup.ps1 @@ -0,0 +1,36 @@ +<# + This script sets up the system to build an installer. +#> + +param ( + [string]$provisioningDir = (Split-Path -parent $PSCommandPath) # Default to script path. +) + +# Turn verbose on, change to "SilentlyContinue" for default behaviour. +$VerbosePreference = "continue" + +# Store the project folder of the script (root of the repo) as $projectDir. +$projectDir = (Get-Item $provisioningDir).parent.FullName + +Import-Module (Join-Path $provisioningDir 'Provisioning.psm1') -Force + +# Obtaining useful tools location. +$npm = "npm" -f $env:SystemDrive +$chocolatey = "$env:ChocolateyInstall\bin\choco.exe" -f $env:SystemDrive + +# Installing required choco packages. +Invoke-Command $chocolatey "install wixtoolset -y" +refreshenv +# The path to WIX can be found in $env:WIX env variable but looks like chocolatey's refreshenv +# is not able to set such variable in this session. As a workaround, we ask the registry +# for such environmental variable and set it so we can use it inside this powershell session. +$wixSetupPath = Join-Path (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' -Name WIX).WIX "bin" +Add-Path $wixSetupPath $true +refreshenv + +Invoke-Command $chocolatey "install msbuild.extensionpack -y" +refreshenv + +# Install electron-packager globally. +# TODO: Define electron-packager invocation in npm scripts. +Invoke-Command $npm "install electron-packager -g" $projectDir From b6c48d83707380226a79d2ee43b4e0b52e02a0a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Hern=C3=A1ndez?= Date: Wed, 9 Oct 2019 12:42:16 +0200 Subject: [PATCH 02/14] GPII-4171: Added initial working implementation --- README.md | 23 ++++- data/artifacts.json | 24 +++++ devTest.js | 17 ++++ index.js | 26 +++++ package.json | 22 ++++ src/artifacts.js | 209 ++++++++++++++++++++++++++++++++++++++ src/main.js | 238 ++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 558 insertions(+), 1 deletion(-) create mode 100644 data/artifacts.json create mode 100644 devTest.js create mode 100644 index.js create mode 100644 package.json create mode 100644 src/artifacts.js create mode 100644 src/main.js diff --git a/README.md b/README.md index b69fdf1..ab422eb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,23 @@ # gpii-windows-installer -The official installer for the GPII on Windows + +The official installer for the GPII on Windows. +This code produces msi installers based on a given set of artifacts. + +At this moment, the code does the following: + +1. Download and populate the artifacts +1. Create a build folder where the installer is going to be created +1. Run npm install on gpii-app +1. Create the electron package +1. Run MSBuild to create the installer + +In addition to these, I need to finish implementing the logic for copying some files (wix merge modules, reset to standard file, etc) into specific folders. + +## Running the code + +The easiest way to run this code is from the VM that you can set up by running `vagrant up` +The resulting VM includes the required dependencies to perform the build process. + +When the VM is ready, you can run `node devTest.js` and the installer will be created automatically. + +Take into account that the build process may take some time (around 10 minutes). diff --git a/data/artifacts.json b/data/artifacts.json new file mode 100644 index 0000000..4514dea --- /dev/null +++ b/data/artifacts.json @@ -0,0 +1,24 @@ +[ + { + "id": "gpii-app", + "repo": "https://github.com/javihernandez/gpii-app", + "hash": "be58bf0160d099c83b1b01c4009b5414a2ef343d", + "build": false + }, + { + "id": "gpii-wix-installer", + "repo": "https://github.com/javihernandez/gpii-wix-installer", + "hash": "c33dd7ddf904a46b3439293096498aa5ae821a37", + "build": false + }, + { + "id": "morphic-sharex-installer", + "repo": "https://github.com/javihernandez/morphic-sharex-installer", + "hash": "28ecaf0fedb7db8c1abbe5d82a98b3de104feaa5", + "build": { + "cmd": "powershell.exe", + "args": ["./build.ps1"], + "outputFile": "sharex.msm" + } + } +] diff --git a/devTest.js b/devTest.js new file mode 100644 index 0000000..35c5823 --- /dev/null +++ b/devTest.js @@ -0,0 +1,17 @@ +var fluid = require("infusion"); + +var gpii = fluid.registerNamespace("gpii"); + +require("./index.js"); + +//fluid.logObjectRenderChars = 1200000; + +var m = gpii.installer({}); + +//m.npmInstall(); + +//m.electronPackager(); +//m.runMsbuild(); + +//m.populateArtifacts(); +//console.log("## m: " + JSON.stringify(m, null, 2)); diff --git a/index.js b/index.js new file mode 100644 index 0000000..a5e0cc2 --- /dev/null +++ b/index.js @@ -0,0 +1,26 @@ +/*! +* Morphic automatic installer +* +* Copyright 2019 Raising the Floor - US +* +* Licensed under the New BSD license. You may not use this file except in +* compliance with this License. +* +* The R&D leading to these results received funding from the +* Department of Education - Grant H421A150005 (GPII-APCP). However, +* these results do not necessarily represent the policy of the +* Department of Education, and you should not assume endorsement by the +* Federal Government. +* +* You may obtain a copy of the License at +* https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ +"use strict" + +var fluid = require("infusion"); + +fluid.module.register("morphic-installer", __dirname, require); + +var morphic = fluid.registerNamespace("morphic"); + +require("./src/main.js"); diff --git a/package.json b/package.json new file mode 100644 index 0000000..bade210 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "gpii-windows-installer", + "version": "0.1.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/gpii/gpii-windows-installer.git" + }, + "license": "BSD-3-Clause", + "dependencies": { + "adm-zip": "0.4.13", + "electron-packager": "14.0.4", + "fs-extra": "8.1.0", + "infusion": "3.0.0-dev.20190328T144119Z.ec44dbfab", + "json5": "2.1.0", + "node-powershell": "4.0.0", + "request": "2.88.0" + } +} diff --git a/src/artifacts.js b/src/artifacts.js new file mode 100644 index 0000000..db18432 --- /dev/null +++ b/src/artifacts.js @@ -0,0 +1,209 @@ +/* +* artifacts.js - Fluid components that allow us to deal with artifacts +* +* Copyright 2019 Raising the Floor - US +* +* Licensed under the New BSD license. You may not use this file except in +* compliance with this License. +* +* The R&D leading to these results received funding from the +* Department of Education - Grant H421A150005 (GPII-APCP). However, +* these results do not necessarily represent the policy of the +* Department of Education, and you should not assume endorsement by the +* Federal Government. +* +* You may obtain a copy of the License at +* https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ +"use strict" + +var fluid = require("infusion"), + admZip = require("adm-zip"), + spawn = require("child_process").spawn, + fs = require("fs"), + path = require("path"), + process = require("process"), + request = require("request"); + +var gpii = fluid.registerNamespace("gpii"); + +fluid.defaults("gpii.installer.artifact", { + gradeNames: "fluid.modelComponent", + defaultOutputPath: path.join(process.cwd(), "artifacts"), + artifactData: null, + model: { + artifactFolder: null + }, + invokers: { + formatDownloadUrl: { + funcName: "gpii.installer.artifact.formatDownloadUrl", + args: ["{that}.options.artifactData"] + }, + populate: { + func: "{that}.download", + args: ["{that}"] + }, + download: { + funcName: "gpii.installer.artifact.download", + args: [ + "{that}", + "{that}.options.artifactData", + "{that}.options.defaultOutputPath", + "{that}.events.onDownloaded", + "{that}.events.onError" + ] + }, + unzip: { + funcName: "gpii.installer.artifact.unzip", + args: [ + "{arguments}.0", + "{that}.options.defaultOutputPath", + "{that}.events.onUnzipped", + "{that}.events.onError" + ] + }, + build: { + funcName: "gpii.installer.artifact.build", + args: [ + "{arguments}.0", + "{that}.options.artifactData.build", + "{that}.events.onBuildFinished", + "{that}.events.onError" + ] + } + }, + events: { + onDownloaded: null, + onUnzipped: null, + onBuildFinished: null, + onPopulated: null, + onError: null + }, + listeners: { + "onCreate.download": "{that}.download", + "onDownloaded.unzip": { + func: "{that}.unzip", + args: "{arguments}.0" + }, + "onUnzipped.updateArtifactFolder": { + changePath: "{that}.model.artifactFolder", + value: "{arguments}.0" + }, + "onUnzipped.build": { + func: "{that}.build", + args: "{arguments}.0" + }, + "onBuildFinished.fireOnPopulated": { + func: "{that}.events.onPopulated.fire", + args: ["Artifact ", "{that}.options.artifactData.id", " has been populated"] + }, + "onPopulated.log": { + funcName: "fluid.log", + args: ["Artifact ", "{that}.options.artifactData.id", " has been populated"] + } + } +}); + +gpii.installer.artifact.formatDownloadUrl = function (artifact) { + return artifact.repo + path.join("/archive", artifact.hash + ".zip"); +}; + +/** +* Download a zip file into a specific location. +* @param {Artifact} artifactId - The id of the artifact to download. +*/ +gpii.installer.artifact.download = function (that, artifact, defaultOutputPath, event, error) { + var downloadUrl = that.formatDownloadUrl(artifact); + // TODO: can we provide the resolved path in a different way? + var outputPath = fluid.module.resolvePath(defaultOutputPath); + var outputFile = path.join(outputPath, artifact.hash + ".zip"); + + if (!fs.existsSync(outputPath)) fs.mkdirSync(outputPath); + + var outStream = fs.createWriteStream(outputFile); + + var req = request.get({ + uri: downloadUrl, + gzip: true + }); + + fluid.log("Downloading ", downloadUrl); + + req.pipe(outStream); + + req.on("error", function (err) { + var err = "Couldn't download artifact, error was: " + err; + outStream.close(); + outStream = null; + error.fire(err); + fluid.fail(err); + }); + + req.on("end", function () { + outStream.close(); + outStream = null; + event.fire(outputFile); + }); +}; + +gpii.installer.artifact.unzip = function (zipFile, outputPath, event, error) { + fluid.log("Unzipping ", zipFile); + + var resolvedOutputPath = fluid.module.resolvePath(outputPath); + /* There is a try/catch block here since I'm getting random errors while using + * the adm-zip library. + * + * The problem only happens occasionally but the problem is that the script crashes + * just saying "FATAL ERROR: Uncaught exception undefined". + * + * This is completely meaningless to the person who is running this code and + * seeing the program crashing right after getting the error above. + * I've also tried using the async version of extractAllTo (actually undocumented), + * and didn't help much since I'm getting false errors or errors that are not fatal. + * + * For these reasons, I think that this try/catch could brings some "sanity" to the + * situation. Maybe we should take a closer look at other alternatives + * to unzip files in the future such as https://www.npmjs.com/package/decompress-zip + * or https://www.npmjs.com/package/unzipper. + */ + try { + var zip = new admZip(zipFile); + // This gets the top-level directory and removes the ending slash coming from the entryName + var unzippedArtifactFolder = zip.getEntries()[0].entryName.slice(0, -1); + zip.extractAllTo(resolvedOutputPath, true); + // Remove the hash reference for the folder name. e.g.: gpii-app-92f9b5e1ba01fc2b39f92d235bfa4b64d60108c5 to gpii-app + var finalArtifactFolder = unzippedArtifactFolder.slice(0, unzippedArtifactFolder.lastIndexOf("-")); + // Rename the unzipped artifact folder + fs.renameSync(path.join(resolvedOutputPath, unzippedArtifactFolder), path.join(resolvedOutputPath, finalArtifactFolder)); + // Remove the zipFile + fs.unlinkSync(zipFile); + event.fire(path.join(resolvedOutputPath, finalArtifactFolder)); + } catch (err) { + fluid.log("Couldn't unzip", zipFile, "Error was:", err); + error.fire("Couldn't unzip " + zipFile); + } +}; + +gpii.installer.artifact.build = function (folder, build, event, error) { + if (!build) { + fluid.log("Skipping build of ", folder); + event.fire(); + } else { + fluid.log("Building ", folder); + var buildC = spawn(build.cmd, build.args, {shell: true, cwd: folder}); + buildC.stdout.on("data", function (data) { + // I know, this if statement is weird, but it actually prevents us from + // printing empty lines coming from the execution of a powershell script. + if (data.toString().trim()) fluid.log(data.toString()); + }); + + buildC.stderr.on("data", function (data) { + fluid.log(data.toString()); + }); + + buildC.on("close", function (code) { + fluid.log("Child process exited with code: ", code); + code ? error.fire("Couldn't build " + folder + " - Check above for errors"): event.fire(code) + }); + } +}; diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..b7bb527 --- /dev/null +++ b/src/main.js @@ -0,0 +1,238 @@ +/* +* main.js - Main fluid components of gpii windows installer +* +* Copyright 2019 Raising the Floor - US +* +* Licensed under the New BSD license. You may not use this file except in +* compliance with this License. +* +* The R&D leading to these results received funding from the +* Department of Education - Grant H421A150005 (GPII-APCP). However, +* these results do not necessarily represent the policy of the +* Department of Education, and you should not assume endorsement by the +* Federal Government. +* +* You may obtain a copy of the License at +* https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ +"use strict" + +var fluid = require("infusion"), + fs = require("fs"), + fse = require("fs-extra"), + spawn = require("child_process").spawn, + path = require("path"), + powershell = require("node-powershell"); + +require("./artifacts.js"); +fluid.setLogging(true); + +var gpii = fluid.registerNamespace("gpii"); + +fluid.defaults("gpii.installer", { + gradeNames: "fluid.component", + artifactsData: fluid.require("%gpii-windows-installer/data/artifacts.json"), + artifacts: ["gpii-app", "gpii-wix-installer", "morphic-sharex-installer"], // TODO: Just load the json file + artifactsFolder: path.join(fluid.module.resolvePath("%gpii-windows-installer"), "artifacts"), + resetToStandardFile: null, // TODO: This will be part of the artifacts.json file + buildFolder: "c:/installer/", + invokers: { + populateArtifacts: { + funcName: "gpii.installer.populateArtifacts", + args: ["{that}", "{arguments}.0"] + }, + prepareBuildFolder: { + funcName: "gpii.installer.prepareBuildFolder", + args: ["{that}"] + }, + npmInstall: { + funcName: "gpii.installer.npmInstall", + args: ["{that}"] + }, + electronPackager: { + funcName: "gpii.installer.electronPackager", + args: ["{that}"] + }, + runMsbuild: { + funcName: "gpii.installer.runMsbuild", + args: ["{that}"] + } + }, + events: { + onPopulatedArtifacts: null, + onBuildFolderReady: null, + onNpmInstallFinished: null, + onPackaged: null, + onError: null + }, + listeners: { + "onCreate.populateArtifacts": { + func: "{that}.populateArtifacts", + args: "{that}.options.artifacts" + }, + "onPopulatedArtifacts.logResult": { + funcName: "fluid.log", + args: ["Artifacts successfully populated: ", "{arguments}.0"] + }, + "onPopulatedArtifacts.prepareBuildFolder": "{that}.prepareBuildFolder", + "onBuildFolderReady.runNpmInstall": "{that}.npmInstall", + "onNpmInstallFinished.logResult": { + funcName: "fluid.log", + args: ["npm install process succeed!"] + }, + "onNpmInstallFinished.runElectronPackager": "{that}.electronPackager", + "onPackaged.logResult": { + funcName: "fluid.log", + args: ["Morphic-App successfully packaged: ", "{arguments}.0"] + }, + "onPackaged.runMsbuild": "{that}.runMsbuild", + "onError.logError": { + funcName: "fluid.log", + args: "{arguments}.0" + } + } +}); + +gpii.installer.populateArtifacts = function (that, artifacts) { + // clean the artifacts folder + if (fs.existsSync(that.options.artifactsFolder)) { + fse.removeSync(that.options.artifactsFolder); + } + + var sequence = []; + + fluid.each(artifacts, function (artifact) { + fluid.log("Populating: ", artifact); + var promise = fluid.promise(); + var artifact = gpii.installer.artifact({ + artifactData: gpii.installer.getArtifactById(that.options.artifactsData, artifact), + }); + + artifact.events.onPopulated.addListener(function () { + promise.resolve(); + }); + artifact.events.onError.addListener(function (err) { + promise.reject(err); + }); + + sequence.push(promise); + }); + + fluid.promise.sequence(sequence).then(function (result) { + that.events.onPopulatedArtifacts.fire(artifacts); + }, function (err) { + that.events.onError.fire("An error occurred while trying to populate the artifacts. The error was: " + err); + }); +}; + +gpii.installer.getArtifactById = function (artifacts, id) { + return fluid.find(artifacts, function (artifact) { + if (artifact.id === id) { + return artifact; + } + }, null); +}; + +gpii.installer.prepareBuildFolder = function (that) { + if (fs.existsSync(that.options.buildFolder)) { + fse.removeSync(that.options.buildFolder); + } + // Copy gpii-wix-installer to c:/installer + fse.copySync(path.join(that.options.artifactsFolder, "gpii-wix-installer"), that.options.buildFolder); + // Copy gpii-app to c:/installer/gpii-app + fse.copySync(path.join(that.options.artifactsFolder, "gpii-app"), path.join(that.options.buildFolder, "gpii-app")); + that.events.onBuildFolderReady.fire(); +}; + +gpii.installer.npmInstall = function (that) { + var buildC = spawn("npm", ["install"], {shell: true, cwd: path.join(that.options.buildFolder, "gpii-app")}); + buildC.stdout.on("data", function (data) { + // I know, this if statement is weird, but it actually prevents us from + // printing empty lines coming from the execution of a powershell script. + if (data.toString().trim()) fluid.log(data.toString()); + }); + + buildC.stderr.on("data", function (data) { + fluid.log(data.toString()); + }); + + buildC.on("close", function (code) { + fluid.log("Child process exited with code: ", code); + that.events.onNpmInstallFinished.fire(); + // TODO: error handling + //code ? error.fire("Couldn't build " + folder + " - Check above for errors"): event.fire(code) + }); +}; + +gpii.installer.electronPackager = function (that) { + var packager = require("electron-packager"); + var options = { + "arch": "ia32", + "platform": "win32", + "dir": path.join(that.options.buildFolder, "gpii-app"), + "app-copyright": "Raising the Floor - International Association", + "name": "morphic-app", + "out": path.join(that.options.buildFolder, "staging"), + "overwrite": true, + "prune": false, + "version": "1.3.2", + "version-string":{ + "CompanyName": "Raising the Floor - International Association", + "FileDescription": "Morphic-App", /*This is what display windows on task manager, shortcut and process*/ + "OriginalFilename": "morphic-app.exe", + "ProductName": "Morphic-App", + "InternalName": "Morphic-App" + } + }; + + var packagerPromise = fluid.toPromise(packager(options, function (err, appPaths) { + // TODO: error handling + fluid.log("## packaged electron app"); + return appPaths; + })); + + fluid.promise.map(packagerPromise, function (appPaths) { + fluid.log(appPaths); + fse.renameSync(path.join(that.options.buildFolder, "staging", "morphic-app-win32-ia32"), path.join(that.options.buildFolder, "staging", "windows")); + that.events.onPackaged.fire(appPaths); + }); +}; + +gpii.installer.runMsbuild = function (that) { + // create output and temp folders in c:/installer + var outputFolder = path.join(that.options.buildFolder, "output"); + var tempFolder = path.join(that.options.buildFolder, "temp"); + + fs.existsSync(outputFolder)? null: fs.mkdirSync(outputFolder); + fs.existsSync(tempFolder)? null: fs.mkdirSync(tempFolder); + + var ps = new powershell({ + executionPolicy: "Bypass", + noProfile: true, + verbose: true + }); + + // Redirect output + ps.streams.stdout.on("data", function (data) { + fluid.log(data); + }); + + // This is ugly, yes, but so far is the best way to prepare the shell, this is: + // 1. load the env variables by calling vcbuildtools_msbuild + // 2. guess the msbuild command path - this probably can also be retrieved via node + // 3. call msbuild + // TODO: Explore other ways to achieve this + ps.addCommand("Import-Module .\\provisioning\\Provisioning.psm1 -Force"); + ps.addCommand("Invoke-Environment 'C:\\Program Files (x86)\\Microsoft Visual C++ Build Tools\\vcbuildtools_msbuild.bat'"); + ps.addCommand("$setupDir = Join-Path " + that.options.buildFolder + " 'setup'"); + ps.addCommand("$msbuild = Get-MSBuild '4.0'"); + //ps.addCommand("Write-Output 'setupDir:' $setupDir ' msbuild:'$msbuild"); + ps.addCommand("Invoke-Command $msbuild 'setup.msbuild' $setupDir"); + ps.invoke().then(function (result) { + fluid.log("MSBuild complete: ", result); + ps.dispose(); + }, function (err) { + fluid.log("There was an error running MSBuild: ", err); + ps.dispose(); + }); +}; From d5c62acdb1dd7c267564bb57f59aa4863cb3519b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Hern=C3=A1ndez?= Date: Wed, 9 Oct 2019 12:45:11 +0200 Subject: [PATCH 03/14] GPII-4171: Added gitignore --- .gitignore | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..471cfe9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.vagrant +artifacts +node_modules +package-lock.json +provisioning/Chocolatey.ps1 +provisioning/CouchDB.ps1 +provisioning/Npm.ps1 +provisioning/Provisioning.psm1 +provisioning/couchdb-2.3.0.msi From e76130358a752496556c8ae1ac7e1df61157fb80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Hern=C3=A1ndez?= Date: Mon, 6 Apr 2020 20:25:23 +0200 Subject: [PATCH 04/14] GPII-4171: Reworked implementation of artifacts and added some more bits - Added windowsService component that deals with the service build and copying the files to the target folders - Reworked the way the main component works - Reduced the size on disk of the final installer - Added dedupe-infusion and making use it - Updated adm-zip dependency - Some more minor updates --- data/artifacts.json | 24 ------ data/artifacts.json5 | 46 ++++++++++++ devTest.js | 1 + index.js | 4 +- package.json | 3 +- src/artifacts.js | 166 +++++++++++++++++------------------------- src/main.js | 125 +++++++++++++++++++++++-------- src/windowsService.js | 165 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 377 insertions(+), 157 deletions(-) delete mode 100644 data/artifacts.json create mode 100644 data/artifacts.json5 create mode 100644 src/windowsService.js diff --git a/data/artifacts.json b/data/artifacts.json deleted file mode 100644 index 4514dea..0000000 --- a/data/artifacts.json +++ /dev/null @@ -1,24 +0,0 @@ -[ - { - "id": "gpii-app", - "repo": "https://github.com/javihernandez/gpii-app", - "hash": "be58bf0160d099c83b1b01c4009b5414a2ef343d", - "build": false - }, - { - "id": "gpii-wix-installer", - "repo": "https://github.com/javihernandez/gpii-wix-installer", - "hash": "c33dd7ddf904a46b3439293096498aa5ae821a37", - "build": false - }, - { - "id": "morphic-sharex-installer", - "repo": "https://github.com/javihernandez/morphic-sharex-installer", - "hash": "28ecaf0fedb7db8c1abbe5d82a98b3de104feaa5", - "build": { - "cmd": "powershell.exe", - "args": ["./build.ps1"], - "outputFile": "sharex.msm" - } - } -] diff --git a/data/artifacts.json5 b/data/artifacts.json5 new file mode 100644 index 0000000..1ff1390 --- /dev/null +++ b/data/artifacts.json5 @@ -0,0 +1,46 @@ +{ + "gpii-app": { + "type": "gpii.installer.artifact.githubRepoDownloader", + "options": { + "repo": "https://github.com/javihernandez/gpii-app", + "hash": "92f9b5e1ba01fc2b39f92d235bfa4b64d60108c5", // GPII-4004 + "output": "gpii-app" + } + }, + "gpii-wix-installer": { + "type": "gpii.installer.artifact.githubRepoDownloader", + "options": { + "repo": "https://github.com/javihernandez/gpii-wix-installer", + "hash": "5effd7a53430aca4654b0d44b36f059f5ab14acf", // GPII-4004 + "output": "gpii-wix-installer" + } + }, + "morphic-sharex-installer": { + "type": "gpii.installer.artifact.downloader", + "options": { + "downloadUrl": "https://github.com/javihernandez/morphic-sharex-installer/releases/download/0.1/sharex.msm", + "output": "sharex.msm" + } + }, + "morphic-documorph-installer": { + "type": "gpii.installer.artifact.downloader", + "options": { + "downloadUrl": "https://github.com/javihernandez/morphic-documorph-installer/releases/download/0.1/documorph.msm", + "output": "documorph.msm" + } + }, + "gpii-filebeat-installer": { + "type": "gpii.installer.artifact.downloader", + "options": { + "downloadUrl": "https://github.com/stegru/gpii-filebeat-installer/releases/download/1.0.0/filebeat.msm", + "output": "filebeat.msm" + } + }, + "resetToStandardFile": { + "type": "gpii.installer.artifact.downloader", + "options": { + "downloadUrl": "https://raw.githubusercontent.com/GPII/universal/master/testData/defaultSettings/defaultSettings.win32.json5", + "output": "defaultSettings.json5" + } + } +} diff --git a/devTest.js b/devTest.js index 35c5823..f00cd6c 100644 --- a/devTest.js +++ b/devTest.js @@ -7,6 +7,7 @@ require("./index.js"); //fluid.logObjectRenderChars = 1200000; var m = gpii.installer({}); +//console.log("#### buildFolder: ", m.options.buildFolder); //m.npmInstall(); diff --git a/index.js b/index.js index a5e0cc2..6978d89 100644 --- a/index.js +++ b/index.js @@ -19,8 +19,8 @@ var fluid = require("infusion"); -fluid.module.register("morphic-installer", __dirname, require); +fluid.module.register("gpii-windows-installer", __dirname, require); -var morphic = fluid.registerNamespace("morphic"); +var gpii = fluid.registerNamespace("gpii"); require("./src/main.js"); diff --git a/package.json b/package.json index bade210..f4a88c3 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ }, "license": "BSD-3-Clause", "dependencies": { - "adm-zip": "0.4.13", + "adm-zip": "0.4.14", + "dedupe-infusion": "1.0.0", "electron-packager": "14.0.4", "fs-extra": "8.1.0", "infusion": "3.0.0-dev.20190328T144119Z.ec44dbfab", diff --git a/src/artifacts.js b/src/artifacts.js index db18432..63ded88 100644 --- a/src/artifacts.js +++ b/src/artifacts.js @@ -21,6 +21,7 @@ var fluid = require("infusion"), admZip = require("adm-zip"), spawn = require("child_process").spawn, fs = require("fs"), + fse = require("fs-extra"), path = require("path"), process = require("process"), request = require("request"); @@ -30,97 +31,78 @@ var gpii = fluid.registerNamespace("gpii"); fluid.defaults("gpii.installer.artifact", { gradeNames: "fluid.modelComponent", defaultOutputPath: path.join(process.cwd(), "artifacts"), - artifactData: null, - model: { - artifactFolder: null - }, + events: { + onPopulated: null, + onError: null + } +}); + +fluid.defaults("gpii.installer.artifact.downloader", { + gradeNames: "gpii.installer.artifact", + downloadUrl: null, invokers: { - formatDownloadUrl: { - funcName: "gpii.installer.artifact.formatDownloadUrl", - args: ["{that}.options.artifactData"] - }, - populate: { - func: "{that}.download", - args: ["{that}"] - }, download: { - funcName: "gpii.installer.artifact.download", - args: [ - "{that}", - "{that}.options.artifactData", - "{that}.options.defaultOutputPath", - "{that}.events.onDownloaded", - "{that}.events.onError" - ] - }, + funcName: "gpii.installer.artifact.download" + } + }, + events: { + onDownloaded: null + }, + listeners: { + "onCreate.download": { + func: "{that}.download", + args: [ "{that}.options.downloadUrl", "{that}.options.defaultOutputPath", + "{that}.options.output", "{that}.events.onPopulated", "{that}.events.onError" ] + } + } +}); + +fluid.defaults("gpii.installer.artifact.githubRepoDownloader", { + gradeNames: "gpii.installer.artifact.downloader", + repo: null, + hash: null, + members: { + zipFile: "@expand:fluid.add({that}.options.hash, .zip)" + }, + invokers: { unzip: { funcName: "gpii.installer.artifact.unzip", - args: [ - "{arguments}.0", - "{that}.options.defaultOutputPath", - "{that}.events.onUnzipped", - "{that}.events.onError" - ] - }, - build: { - funcName: "gpii.installer.artifact.build", - args: [ - "{arguments}.0", - "{that}.options.artifactData.build", - "{that}.events.onBuildFinished", - "{that}.events.onError" - ] + args: ["unzip function called"] } }, events: { - onDownloaded: null, - onUnzipped: null, - onBuildFinished: null, - onPopulated: null, - onError: null + onUnzipped: null }, listeners: { - "onCreate.download": "{that}.download", - "onDownloaded.unzip": { - func: "{that}.unzip", - args: "{arguments}.0" - }, - "onUnzipped.updateArtifactFolder": { - changePath: "{that}.model.artifactFolder", - value: "{arguments}.0" - }, - "onUnzipped.build": { - func: "{that}.build", - args: "{arguments}.0" + "onCreate.formatDownloadUrl": { + funcName: "gpii.installer.artifact.formatGithubDownloadUrl", + args: ["{that}"] }, - "onBuildFinished.fireOnPopulated": { - func: "{that}.events.onPopulated.fire", - args: ["Artifact ", "{that}.options.artifactData.id", " has been populated"] + "onCreate.download": { + func: "{that}.download", + args: ["{that}.options.downloadUrl", "{that}.options.defaultOutputPath", + "{that}.zipFile", + "{that}.events.onDownloaded", "{that}.events.onError" ], + priority:"after:onCreate.formatDownloadUrl" }, - "onPopulated.log": { - funcName: "fluid.log", - args: ["Artifact ", "{that}.options.artifactData.id", " has been populated"] + "onDownloaded.unzip": { + funcName: "gpii.installer.artifact.unzip", + args: ["{that}.zipFile", "{that}.options.defaultOutputPath", + "{that}.events.onPopulated", "{that}.events.onError"] } } }); -gpii.installer.artifact.formatDownloadUrl = function (artifact) { - return artifact.repo + path.join("/archive", artifact.hash + ".zip"); -}; -/** -* Download a zip file into a specific location. -* @param {Artifact} artifactId - The id of the artifact to download. -*/ -gpii.installer.artifact.download = function (that, artifact, defaultOutputPath, event, error) { - var downloadUrl = that.formatDownloadUrl(artifact); +gpii.installer.artifact.download = function (downloadUrl, defaultOutputPath, outputFile, event, error) { // TODO: can we provide the resolved path in a different way? var outputPath = fluid.module.resolvePath(defaultOutputPath); - var outputFile = path.join(outputPath, artifact.hash + ".zip"); + var outputFilePath = path.join(outputPath, outputFile); if (!fs.existsSync(outputPath)) fs.mkdirSync(outputPath); + if (fs.existsSync(outputFilePath)) fse.removeSync(outputFilePath); - var outStream = fs.createWriteStream(outputFile); + var outStream = fs.createWriteStream(outputFilePath); var req = request.get({ uri: downloadUrl, @@ -146,10 +128,16 @@ gpii.installer.artifact.download = function (that, artifact, defaultOutputPath, }); }; -gpii.installer.artifact.unzip = function (zipFile, outputPath, event, error) { - fluid.log("Unzipping ", zipFile); - var resolvedOutputPath = fluid.module.resolvePath(outputPath); +gpii.installer.artifact.formatGithubDownloadUrl = function (that) { + that.options.downloadUrl = that.options.repo + path.join("/archive", that.options.hash + ".zip"); +}; + +gpii.installer.artifact.unzip = function (file, defaultOutputPath, event, error) { + fluid.log("Unzipping ", file); + var zipFile = path.join(defaultOutputPath, file); + + var outputPath = fluid.module.resolvePath(defaultOutputPath); /* There is a try/catch block here since I'm getting random errors while using * the adm-zip library. * @@ -170,40 +158,18 @@ gpii.installer.artifact.unzip = function (zipFile, outputPath, event, error) { var zip = new admZip(zipFile); // This gets the top-level directory and removes the ending slash coming from the entryName var unzippedArtifactFolder = zip.getEntries()[0].entryName.slice(0, -1); - zip.extractAllTo(resolvedOutputPath, true); + if (fs.existsSync(path.join(outputPath, unzippedArtifactFolder))) fse.removeSync(path.join(outputPath, unzippedArtifactFolder)); + zip.extractAllTo(outputPath, true); // Remove the hash reference for the folder name. e.g.: gpii-app-92f9b5e1ba01fc2b39f92d235bfa4b64d60108c5 to gpii-app var finalArtifactFolder = unzippedArtifactFolder.slice(0, unzippedArtifactFolder.lastIndexOf("-")); + if (fs.existsSync(path.join(outputPath, finalArtifactFolder))) fse.removeSync(path.join(outputPath, finalArtifactFolder)); // Rename the unzipped artifact folder - fs.renameSync(path.join(resolvedOutputPath, unzippedArtifactFolder), path.join(resolvedOutputPath, finalArtifactFolder)); + fs.renameSync(path.join(outputPath, unzippedArtifactFolder), path.join(outputPath, finalArtifactFolder)); // Remove the zipFile fs.unlinkSync(zipFile); - event.fire(path.join(resolvedOutputPath, finalArtifactFolder)); + event.fire(path.join(outputPath, finalArtifactFolder)); } catch (err) { fluid.log("Couldn't unzip", zipFile, "Error was:", err); error.fire("Couldn't unzip " + zipFile); } }; - -gpii.installer.artifact.build = function (folder, build, event, error) { - if (!build) { - fluid.log("Skipping build of ", folder); - event.fire(); - } else { - fluid.log("Building ", folder); - var buildC = spawn(build.cmd, build.args, {shell: true, cwd: folder}); - buildC.stdout.on("data", function (data) { - // I know, this if statement is weird, but it actually prevents us from - // printing empty lines coming from the execution of a powershell script. - if (data.toString().trim()) fluid.log(data.toString()); - }); - - buildC.stderr.on("data", function (data) { - fluid.log(data.toString()); - }); - - buildC.on("close", function (code) { - fluid.log("Child process exited with code: ", code); - code ? error.fire("Couldn't build " + folder + " - Check above for errors"): event.fire(code) - }); - } -}; diff --git a/src/main.js b/src/main.js index b7bb527..fba4278 100644 --- a/src/main.js +++ b/src/main.js @@ -17,25 +17,43 @@ */ "use strict" -var fluid = require("infusion"), +var dedupe = require("dedupe-infusion"), + fluid = require("infusion"), fs = require("fs"), fse = require("fs-extra"), spawn = require("child_process").spawn, path = require("path"), powershell = require("node-powershell"); -require("./artifacts.js"); +require("json5/lib/register"); + fluid.setLogging(true); var gpii = fluid.registerNamespace("gpii"); +require("./artifacts.js"); +require("./windowsService.js"); + +var artifactsData = fluid.require("%gpii-windows-installer/data/artifacts.json5"); + fluid.defaults("gpii.installer", { gradeNames: "fluid.component", - artifactsData: fluid.require("%gpii-windows-installer/data/artifacts.json"), - artifacts: ["gpii-app", "gpii-wix-installer", "morphic-sharex-installer"], // TODO: Just load the json file + artifactsData: artifactsData, artifactsFolder: path.join(fluid.module.resolvePath("%gpii-windows-installer"), "artifacts"), resetToStandardFile: null, // TODO: This will be part of the artifacts.json file buildFolder: "c:/installer/", + components: { + windowsServiceBuilder: { + type: "gpii.installer.windowsServiceBuilder", + options: { + buildFolder: "{installer}.options.buildFolder", + events: { + onWindowsServiceReady: "{installer}.events.onWindowsServiceReady" + } + }, + createOnEvent: "onPackaged" + } + }, invokers: { populateArtifacts: { funcName: "gpii.installer.populateArtifacts", @@ -50,12 +68,16 @@ fluid.defaults("gpii.installer", { args: ["{that}"] }, electronPackager: { - funcName: "gpii.installer.electronPackager", - args: ["{that}"] + funcName: "gpii.installer.electronPackager", + args: ["{that}"] + }, + shrinkSize: { + funcName: "gpii.installer.shrinkSize", + args: ["{that}"] }, runMsbuild: { - funcName: "gpii.installer.runMsbuild", - args: ["{that}"] + funcName: "gpii.installer.runMsbuild", + args: ["{that}"] } }, events: { @@ -63,52 +85,66 @@ fluid.defaults("gpii.installer", { onBuildFolderReady: null, onNpmInstallFinished: null, onPackaged: null, + onWindowsServiceReady: null, + onShrunk: null, onError: null }, listeners: { "onCreate.populateArtifacts": { func: "{that}.populateArtifacts", - args: "{that}.options.artifacts" + args: "{that}.options.artifactsData" }, "onPopulatedArtifacts.logResult": { funcName: "fluid.log", - args: ["Artifacts successfully populated: ", "{arguments}.0"] + args: ["Artifacts successfully populated"] }, "onPopulatedArtifacts.prepareBuildFolder": "{that}.prepareBuildFolder", "onBuildFolderReady.runNpmInstall": "{that}.npmInstall", "onNpmInstallFinished.logResult": { funcName: "fluid.log", - args: ["npm install process succeed!"] + args: ["npm install process succeeded"] }, "onNpmInstallFinished.runElectronPackager": "{that}.electronPackager", "onPackaged.logResult": { funcName: "fluid.log", args: ["Morphic-App successfully packaged: ", "{arguments}.0"] }, - "onPackaged.runMsbuild": "{that}.runMsbuild", - "onError.logError": { + "onWindowsServiceReady.logResult": { + funcName: "fluid.log", + args: ["Morphic service successfully created"] + }, + "onWindowsServiceReady.shrinkSize": "{that}.shrinkSize", + "onShrunk.logResult": { funcName: "fluid.log", + args: ["Shrunk size of node_modules folder"] + }, + "onShrunk.runMsbuild": "{that}.runMsbuild", + //"onWindowsServiceReady.runMsbuild": "{that}.runMsbuild", + "onError.logError": { + funcName: "fluid.fail", args: "{arguments}.0" } } }); -gpii.installer.populateArtifacts = function (that, artifacts) { +gpii.installer.populateArtifacts = function (that, artifactsData) { // clean the artifacts folder if (fs.existsSync(that.options.artifactsFolder)) { fse.removeSync(that.options.artifactsFolder); } var sequence = []; + var artifactsList = []; - fluid.each(artifacts, function (artifact) { - fluid.log("Populating: ", artifact); + fluid.each(artifactsData, function (artifactData, artifactName) { + fluid.log("Populating: ", artifactName); var promise = fluid.promise(); - var artifact = gpii.installer.artifact({ - artifactData: gpii.installer.getArtifactById(that.options.artifactsData, artifact), - }); + + var artifact = fluid.invokeGlobalFunction(artifactData.type, artifactData.options); artifact.events.onPopulated.addListener(function () { + fluid.log("Artifact ", artifactName, " has been populated"); + artifactsList.push(artifactName); promise.resolve(); }); artifact.events.onError.addListener(function (err) { @@ -119,20 +155,12 @@ gpii.installer.populateArtifacts = function (that, artifacts) { }); fluid.promise.sequence(sequence).then(function (result) { - that.events.onPopulatedArtifacts.fire(artifacts); + that.events.onPopulatedArtifacts.fire(); }, function (err) { that.events.onError.fire("An error occurred while trying to populate the artifacts. The error was: " + err); }); }; -gpii.installer.getArtifactById = function (artifacts, id) { - return fluid.find(artifacts, function (artifact) { - if (artifact.id === id) { - return artifact; - } - }, null); -}; - gpii.installer.prepareBuildFolder = function (that) { if (fs.existsSync(that.options.buildFolder)) { fse.removeSync(that.options.buildFolder); @@ -158,9 +186,13 @@ gpii.installer.npmInstall = function (that) { buildC.on("close", function (code) { fluid.log("Child process exited with code: ", code); - that.events.onNpmInstallFinished.fire(); + // TODO: error handling - //code ? error.fire("Couldn't build " + folder + " - Check above for errors"): event.fire(code) + if (code) { + that.events.onError.fire("Couldn't finish npm install process - Check above for errors"); + } else { + that.events.onNpmInstallFinished.fire(); + } }); }; @@ -198,6 +230,39 @@ gpii.installer.electronPackager = function (that) { }); }; +// TODO: Rework this in a better way +gpii.installer.shrinkSize = function (that) { + var stagingAppFolder = path.join(that.options.buildFolder, "staging", "windows", "resources", "app"); + var stagingAppModulesFolder = path.join(stagingAppFolder, "node_modules"); + + // 1.- npm prune --production + var buildC = spawn("npm", ["prune", "--production"], {shell: true, cwd: stagingAppFolder}); + buildC.stdout.on("data", function (data) { + // I know, this if statement is weird, but it actually prevents us from + // printing empty lines coming from the execution of a powershell script. + if (data.toString().trim()) fluid.log(data.toString()); + }); + + buildC.stderr.on("data", function (data) { + fluid.log(data.toString()); + }); + + buildC.on("close", function (code) { + fluid.log("Child process exited with code: ", code); + + // TODO: error handling + if (code) { + that.events.onError.fire("Couldn't npm prune - Check above for errors"); + } else { + // 2.- rm node_modules/electron + fse.removeSync(path.join(stagingAppModulesFolder, "electron")); + // 3.- dedupe-infusion + dedupe.dedupeInfusion({node_modules: stagingAppModulesFolder}); + that.events.onShrunk.fire(); + } + }); +}; + gpii.installer.runMsbuild = function (that) { // create output and temp folders in c:/installer var outputFolder = path.join(that.options.buildFolder, "output"); diff --git a/src/windowsService.js b/src/windowsService.js new file mode 100644 index 0000000..7f42bf8 --- /dev/null +++ b/src/windowsService.js @@ -0,0 +1,165 @@ +/* +* windowsService.js - Fluid components that allow us to deal with artifacts +* +* Copyright 2019 Raising the Floor - US +* +* Licensed under the New BSD license. You may not use this file except in +* compliance with this License. +* +* The R&D leading to these results received funding from the +* Department of Education - Grant H421A150005 (GPII-APCP). However, +* these results do not necessarily represent the policy of the +* Department of Education, and you should not assume endorsement by the +* Federal Government. +* +* You may obtain a copy of the License at +* https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ +"use strict" + +var fluid = require("infusion"), + admZip = require("adm-zip"), + spawn = require("child_process").spawn, + fs = require("fs"), + fse = require("fs-extra"), + path = require("path"), + process = require("process"), + request = require("request"); + +var gpii = fluid.registerNamespace("gpii"); + +fluid.defaults("gpii.installer.windowsServiceBuilder", { + gradeNames: "fluid.modelComponent", + buildFolder: null, + members: { + serviceFolder: "@expand:path.join({that}.options.buildFolder, gpii-app, node_modules, gpii-windows, gpii-service)", + serviceModulesFolder: "@expand:path.join({that}.serviceFolder, node_modules)", + serviceOutputFolder: "@expand:path.join({that}.options.buildFolder, staging, windows)" + }, + invokers: { + npmInstall: { + funcName: "gpii.installer.windowsServiceBuilder.npmInstall", + args: ["{that}.serviceFolder", + "{that}.serviceModulesFolder", + "{that}.events.onNpmInstallFinished", + "{that}.events.onError" + ] + }, + createPkg: { + funcName: "gpii.installer.windowsServiceBuilder.createPkg", + args: ["{that}.options.buildFolder", + "{that}.serviceFolder", + "{that}.serviceOutputFolder", + "{that}.events.onPkgCreated", + "{that}.events.onError" + ] + }, + copyFiles: { + funcName: "gpii.installer.windowsServiceBuilder.copyFiles", + args: ["{that}.serviceFolder", + "{that}.serviceModulesFolder", + "{that}.serviceOutputFolder", + "{that}.events.onWindowsServiceReady", + "{that}.events.onError" + ] + } + }, + events: { + onNpmInstallFinished: null, + onPkgCreated: null, + onWindowsServiceReady: null, + onError: null + }, + listeners: { + "onCreate.runNpmInstall": "{that}.npmInstall", + "onNpmInstallFinished.createPkg": "{that}.createPkg", + "onPkgCreated.copyFiles": "{that}.copyFiles", + "onError.fail": { + funcName: "fluid.fail", + args: [] + } + } +}); + +gpii.installer.windowsServiceBuilder.npmInstall = function (serviceFolder, modulesFolder, event, error) { + if (fs.existsSync(modulesFolder)) fse.removeSync(modulesFolder); + + // First, we npm install the service + var buildC = spawn("npm", ["install", "--production"], { + shell: true, + cwd: serviceFolder + }); + buildC.stdout.on("data", function (data) { + // I know, this if statement is weird, but it actually prevents us from + // printing empty lines coming from the execution of a powershell script. + if (data.toString().trim()) fluid.log(data.toString()); + }); + + buildC.stderr.on("data", function (data) { + fluid.log(data.toString()); + }); + + buildC.on("close", function (code) { + fluid.log("npm install windows service process exited with code: ", code); + if (code) error.fire("Couldn't npm install gpii-service") + event.fire(); + }); +}; + +gpii.installer.windowsServiceBuilder.createPkg = function (buildFolder, serviceFolder, serviceOutputFolder, event, error) { + fs.copyFileSync( + path.join(buildFolder, "gpii-app", "provisioning", "service.json5"), + path.join(serviceFolder, "config", "service.json5") + ); + + var buildC = spawn("pkg", ["package.json", "--output", path.join(serviceOutputFolder, "morphic-service.exe")], { + shell: true, + cwd: serviceFolder + }); + buildC.stdout.on("data", function (data) { + // I know, this if statement is weird, but it actually prevents us from + // printing empty lines coming from the execution of a powershell script. + if (data.toString().trim()) fluid.log(data.toString()); + }); + + buildC.stderr.on("data", function (data) { + fluid.log(data.toString()); + }); + + buildC.on("close", function (code) { + fluid.log("compilation of gpii-service process exited with code: ", code); + if (code) { + error.fire("Couldn't compile gpii-service"); + } else { + event.fire(); + } + }); +}; + +gpii.installer.windowsServiceBuilder.copyFiles = function(serviceFolder, modulesFolder, serviceOutputFolder, event, error) { + // The service needs the .node obj files of its deps to be copied + // along with it. Also, need to put the service.json5 file in the + // same folder. + fs.copyFileSync( + path.join(modulesFolder, "os-service", "build", "Release", "service.node"), + path.join(serviceOutputFolder, "service.node") + ); + fs.copyFileSync( + path.join(modulesFolder, "ffi-napi", "build", "Release", "ffi_bindings.node"), + path.join(serviceOutputFolder, "ffi_bindings.node") + ); + fs.copyFileSync( + path.join(modulesFolder, "ref-napi", "build", "Release", "binding.node"), + path.join(serviceOutputFolder, "binding.node") + ); + fs.copyFileSync( + path.join(modulesFolder, "ref-napi", "build", "Release", "nothing.node"), + path.join(serviceOutputFolder, "nothing.node") + ); + fs.copyFileSync( + path.join(serviceFolder, "config", "service.json5"), + path.join(serviceOutputFolder, "service.json5") + ); + + event.fire(); +} From 775f0e2a9e7ac983de0b9e08bab72a45bf632bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Hern=C3=A1ndez?= Date: Mon, 6 Apr 2020 20:29:15 +0200 Subject: [PATCH 05/14] GPII-4171: Updated build script to install pkg globally This is needed for the windows service to be created --- provisioning/Build.ps1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/provisioning/Build.ps1 b/provisioning/Build.ps1 index 47b44dc..77750b4 100644 --- a/provisioning/Build.ps1 +++ b/provisioning/Build.ps1 @@ -69,5 +69,10 @@ try { exit 1 } +$npmCmd = "npm" -f $env:SystemDrive + +## npm install pkg globally +Invoke-Command $npmCmd "install -g pkg" + ## Run npm install Invoke-Command "npm" "install" $mainDir From f1807f8bd5f8424ee6c0db5d40edf0e0f99e3ddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Hern=C3=A1ndez?= Date: Tue, 7 Apr 2020 13:25:42 +0200 Subject: [PATCH 06/14] GPII-4171: Added copying of optional artifacts mechanism Also, added the outputPath in data/artifacts.json5 for this new mechanism to copy the files to the right place. --- data/artifacts.json5 | 16 ++++++++++++---- src/main.js | 25 +++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/data/artifacts.json5 b/data/artifacts.json5 index 1ff1390..28e26ae 100644 --- a/data/artifacts.json5 +++ b/data/artifacts.json5 @@ -1,4 +1,5 @@ { + // No outputPath, the output location is always c:/installer/gpii-app "gpii-app": { "type": "gpii.installer.artifact.githubRepoDownloader", "options": { @@ -7,6 +8,7 @@ "output": "gpii-app" } }, + // No outputPath, the output location is always c:/installer/ "gpii-wix-installer": { "type": "gpii.installer.artifact.githubRepoDownloader", "options": { @@ -15,32 +17,38 @@ "output": "gpii-wix-installer" } }, + // For the rest of the artifacts, we need to provide the outputPath, always + // relative to the buildFolder, by default, c:/installer "morphic-sharex-installer": { "type": "gpii.installer.artifact.downloader", "options": { "downloadUrl": "https://github.com/javihernandez/morphic-sharex-installer/releases/download/0.1/sharex.msm", - "output": "sharex.msm" + "output": "sharex.msm", + "outputPath": "sharex.msm" } }, "morphic-documorph-installer": { "type": "gpii.installer.artifact.downloader", "options": { "downloadUrl": "https://github.com/javihernandez/morphic-documorph-installer/releases/download/0.1/documorph.msm", - "output": "documorph.msm" + "output": "documorph.msm", + "outputPath": "documorph.msm" } }, "gpii-filebeat-installer": { "type": "gpii.installer.artifact.downloader", "options": { "downloadUrl": "https://github.com/stegru/gpii-filebeat-installer/releases/download/1.0.0/filebeat.msm", - "output": "filebeat.msm" + "output": "filebeat.msm", + "outputPath": "filebeat.msm" } }, "resetToStandardFile": { "type": "gpii.installer.artifact.downloader", "options": { "downloadUrl": "https://raw.githubusercontent.com/GPII/universal/master/testData/defaultSettings/defaultSettings.win32.json5", - "output": "defaultSettings.json5" + "output": "defaultSettings.json5", + "outputPath": "staging/windows/resources/app/node_modules/gpii-universal/testData/defaultSettings/defaultSettings.json5" } } } diff --git a/src/main.js b/src/main.js index fba4278..8da02e3 100644 --- a/src/main.js +++ b/src/main.js @@ -75,6 +75,10 @@ fluid.defaults("gpii.installer", { funcName: "gpii.installer.shrinkSize", args: ["{that}"] }, + copyOptionalArtifacts: { + funcName: "gpii.installer.copyOptionalArtifacts", + args: ["{that}"] + }, runMsbuild: { funcName: "gpii.installer.runMsbuild", args: ["{that}"] @@ -87,6 +91,7 @@ fluid.defaults("gpii.installer", { onPackaged: null, onWindowsServiceReady: null, onShrunk: null, + onCopiedOptionalArtifacts: null, onError: null }, listeners: { @@ -118,8 +123,12 @@ fluid.defaults("gpii.installer", { funcName: "fluid.log", args: ["Shrunk size of node_modules folder"] }, - "onShrunk.runMsbuild": "{that}.runMsbuild", - //"onWindowsServiceReady.runMsbuild": "{that}.runMsbuild", + "onShrunk.copyOptionalArtifacts": "{that}.copyOptionalArtifacts", + "onCopiedOptionalArtifacts.logResult": { + funcName: "fluid.log", + args: ["Copied optional artifacts"] + }, + "onCopiedOptionalArtifacts.runMsbuild": "{that}.runMsbuild", "onError.logError": { funcName: "fluid.fail", args: "{arguments}.0" @@ -263,6 +272,18 @@ gpii.installer.shrinkSize = function (that) { }); }; +gpii.installer.copyOptionalArtifacts = function (that) { + fluid.each(that.options.artifactsData, function (artifactData) { + if (artifactData.options.outputPath) { + var source = path.join(that.options.artifactsFolder, artifactData.options.output); + var target = path.join(that.options.buildFolder, artifactData.options.outputPath); + fs.copyFileSync(source, target); + fluid.log("Copied ", source, " to ", target); + } + }); + that.events.onCopiedOptionalArtifacts.fire(); +} + gpii.installer.runMsbuild = function (that) { // create output and temp folders in c:/installer var outputFolder = path.join(that.options.buildFolder, "output"); From 900b9dd3bdf566222854e8ec3dc28caf9d095144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Hern=C3=A1ndez?= Date: Tue, 7 Apr 2020 13:27:38 +0200 Subject: [PATCH 07/14] GPII-4171: Wait for the pipe close event to consider the file downloaded --- src/artifacts.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/artifacts.js b/src/artifacts.js index 63ded88..c680d80 100644 --- a/src/artifacts.js +++ b/src/artifacts.js @@ -111,7 +111,7 @@ gpii.installer.artifact.download = function (downloadUrl, defaultOutputPath, out fluid.log("Downloading ", downloadUrl); - req.pipe(outStream); + var pipe = req.pipe(outStream); req.on("error", function (err) { var err = "Couldn't download artifact, error was: " + err; @@ -124,7 +124,11 @@ gpii.installer.artifact.download = function (downloadUrl, defaultOutputPath, out req.on("end", function () { outStream.close(); outStream = null; - event.fire(outputFile); + // This way we avoid the 'End-of-central-directory signature not found' + // error that sometimes we get. + pipe.on("close", function () { + event.fire(outputFile); + }); }); }; From f0a43356f40a59f49f12f5c625d988d8f6281041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Hern=C3=A1ndez?= Date: Fri, 8 May 2020 12:01:42 +0200 Subject: [PATCH 08/14] GPII-4171: Updated references to gpii-app and gpii-wix-installer --- data/artifacts.json5 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/artifacts.json5 b/data/artifacts.json5 index 28e26ae..3cf90e5 100644 --- a/data/artifacts.json5 +++ b/data/artifacts.json5 @@ -3,8 +3,8 @@ "gpii-app": { "type": "gpii.installer.artifact.githubRepoDownloader", "options": { - "repo": "https://github.com/javihernandez/gpii-app", - "hash": "92f9b5e1ba01fc2b39f92d235bfa4b64d60108c5", // GPII-4004 + "repo": "https://github.com/GPII/gpii-app", + "hash": "2befbfe96a58533a0954a39203dfc8a4b6b16bd5", // master "output": "gpii-app" } }, @@ -13,7 +13,7 @@ "type": "gpii.installer.artifact.githubRepoDownloader", "options": { "repo": "https://github.com/javihernandez/gpii-wix-installer", - "hash": "5effd7a53430aca4654b0d44b36f059f5ab14acf", // GPII-4004 + "hash": "64c2e56e85d9a2e96d0ceefa36489cfba52468e7", // GPII-4402 "output": "gpii-wix-installer" } }, From 93ee6870332b664f70470b5ea5edfc2711e9528c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Hern=C3=A1ndez?= Date: Fri, 8 May 2020 12:04:08 +0200 Subject: [PATCH 09/14] GPII-4171: Removed morphic-sharex-installer artifact --- data/artifacts.json5 | 8 -------- 1 file changed, 8 deletions(-) diff --git a/data/artifacts.json5 b/data/artifacts.json5 index 3cf90e5..920a359 100644 --- a/data/artifacts.json5 +++ b/data/artifacts.json5 @@ -19,14 +19,6 @@ }, // For the rest of the artifacts, we need to provide the outputPath, always // relative to the buildFolder, by default, c:/installer - "morphic-sharex-installer": { - "type": "gpii.installer.artifact.downloader", - "options": { - "downloadUrl": "https://github.com/javihernandez/morphic-sharex-installer/releases/download/0.1/sharex.msm", - "output": "sharex.msm", - "outputPath": "sharex.msm" - } - }, "morphic-documorph-installer": { "type": "gpii.installer.artifact.downloader", "options": { From 484855bc166e8588330d09ab8a06bb708e7d6310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Hern=C3=A1ndez?= Date: Fri, 8 May 2020 12:05:50 +0200 Subject: [PATCH 10/14] GPII-4171: Updated path to gpii-service according to gpii-app/master --- src/windowsService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/windowsService.js b/src/windowsService.js index 7f42bf8..98644a4 100644 --- a/src/windowsService.js +++ b/src/windowsService.js @@ -141,7 +141,7 @@ gpii.installer.windowsServiceBuilder.copyFiles = function(serviceFolder, modules // along with it. Also, need to put the service.json5 file in the // same folder. fs.copyFileSync( - path.join(modulesFolder, "os-service", "build", "Release", "service.node"), + path.join(modulesFolder, "@gpii", "os-service", "build", "Release", "service.node"), path.join(serviceOutputFolder, "service.node") ); fs.copyFileSync( From 3f4a0e22f14982c28f0002ed36ba13796493d803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Hern=C3=A1ndez?= Date: Fri, 8 May 2020 12:06:54 +0200 Subject: [PATCH 11/14] GPII-4171: Removed unnecessary files before creating the installer --- src/artifacts.js | 2 +- src/main.js | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/artifacts.js b/src/artifacts.js index c680d80..754e057 100644 --- a/src/artifacts.js +++ b/src/artifacts.js @@ -125,7 +125,7 @@ gpii.installer.artifact.download = function (downloadUrl, defaultOutputPath, out outStream.close(); outStream = null; // This way we avoid the 'End-of-central-directory signature not found' - // error that sometimes we get. + // error that sometimes we get when downloading zip files. pipe.on("close", function () { event.fire(outputFile); }); diff --git a/src/main.js b/src/main.js index 8da02e3..ddf2d25 100644 --- a/src/main.js +++ b/src/main.js @@ -267,6 +267,16 @@ gpii.installer.shrinkSize = function (that) { fse.removeSync(path.join(stagingAppModulesFolder, "electron")); // 3.- dedupe-infusion dedupe.dedupeInfusion({node_modules: stagingAppModulesFolder}); + // 4 - remove settingsHelper folder - we only need the settingsHelper.exe + // file from the gpii-windows bin folder + fse.removeSync(path.join(stagingAppModulesFolder, "gpii-windows", "settingsHelper")); + // 5 - remove unnecessary things + fse.removeSync(path.join(stagingAppModulesFolder, "gpii-windows", "bin", "settingsHelper.pdb")); + fse.removeSync(path.join(stagingAppModulesFolder, "gpii-windows", "gpii-service", "node_modules")); + fse.removeSync(path.join(stagingAppModulesFolder, "edge-js")); + fse.removeSync(path.join(stagingAppModulesFolder, "gpii-universal", "tests")); + fse.removeSync(path.join(stagingAppModulesFolder, "infusion", "tests")); + that.events.onShrunk.fire(); } }); From a735b6771c79c6d8768e07673a894745810ee96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Hern=C3=A1ndez?= Date: Fri, 8 May 2020 14:14:21 +0200 Subject: [PATCH 12/14] GPII-4171: Updated reference to gpii-wix-installer Also, added some comments to the shrinkSize function --- data/artifacts.json5 | 2 +- src/main.js | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/data/artifacts.json5 b/data/artifacts.json5 index 920a359..e2feb09 100644 --- a/data/artifacts.json5 +++ b/data/artifacts.json5 @@ -13,7 +13,7 @@ "type": "gpii.installer.artifact.githubRepoDownloader", "options": { "repo": "https://github.com/javihernandez/gpii-wix-installer", - "hash": "64c2e56e85d9a2e96d0ceefa36489cfba52468e7", // GPII-4402 + "hash": "b78f22e0ada9e9d0558e154c7416060c325bd1c7", // GPII-4402 "output": "gpii-wix-installer" } }, diff --git a/src/main.js b/src/main.js index ddf2d25..7cf5b2f 100644 --- a/src/main.js +++ b/src/main.js @@ -271,9 +271,16 @@ gpii.installer.shrinkSize = function (that) { // file from the gpii-windows bin folder fse.removeSync(path.join(stagingAppModulesFolder, "gpii-windows", "settingsHelper")); // 5 - remove unnecessary things + // * This is used in case we want to debug the settingsHelper.exe + // utility, but since the build is based on a particular gpii-app + // git ref, we can build anytime the same code and debug from + // the development environment, which is more convenient. fse.removeSync(path.join(stagingAppModulesFolder, "gpii-windows", "bin", "settingsHelper.pdb")); + // * We don't need any of those deps from the gpii-service fse.removeSync(path.join(stagingAppModulesFolder, "gpii-windows", "gpii-service", "node_modules")); + // * On electron we use electron-edge-js fse.removeSync(path.join(stagingAppModulesFolder, "edge-js")); + // * We don't need to ship tests fse.removeSync(path.join(stagingAppModulesFolder, "gpii-universal", "tests")); fse.removeSync(path.join(stagingAppModulesFolder, "infusion", "tests")); From a26c50fb089474af1289b956e9e4410edfa174f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Hern=C3=A1ndez?= Date: Fri, 8 May 2020 19:43:22 +0200 Subject: [PATCH 13/14] GPII-4171: Updated gpii-wix-installer reference --- data/artifacts.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/artifacts.json5 b/data/artifacts.json5 index e2feb09..14a8150 100644 --- a/data/artifacts.json5 +++ b/data/artifacts.json5 @@ -13,7 +13,7 @@ "type": "gpii.installer.artifact.githubRepoDownloader", "options": { "repo": "https://github.com/javihernandez/gpii-wix-installer", - "hash": "b78f22e0ada9e9d0558e154c7416060c325bd1c7", // GPII-4402 + "hash": "878f7272af0d3c18b42042784dd1561f9f3fb728", // GPII-4402 "output": "gpii-wix-installer" } }, From 3e5c8f5002cd5ba10ad1d6f43ca630dd60d55169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Hern=C3=A1ndez?= Date: Tue, 26 May 2020 13:31:10 +0200 Subject: [PATCH 14/14] GPII-4171: Updated gpii-wix-installer reference --- data/artifacts.json5 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/artifacts.json5 b/data/artifacts.json5 index 14a8150..609a548 100644 --- a/data/artifacts.json5 +++ b/data/artifacts.json5 @@ -12,8 +12,8 @@ "gpii-wix-installer": { "type": "gpii.installer.artifact.githubRepoDownloader", "options": { - "repo": "https://github.com/javihernandez/gpii-wix-installer", - "hash": "878f7272af0d3c18b42042784dd1561f9f3fb728", // GPII-4402 + "repo": "https://github.com/GPII/gpii-wix-installer", + "hash": "b98474d53e2e5aa667ed610bb19414e56790593a", "output": "gpii-wix-installer" } },