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 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/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/data/artifacts.json5 b/data/artifacts.json5 new file mode 100644 index 0000000..609a548 --- /dev/null +++ b/data/artifacts.json5 @@ -0,0 +1,46 @@ +{ + // No outputPath, the output location is always c:/installer/gpii-app + "gpii-app": { + "type": "gpii.installer.artifact.githubRepoDownloader", + "options": { + "repo": "https://github.com/GPII/gpii-app", + "hash": "2befbfe96a58533a0954a39203dfc8a4b6b16bd5", // master + "output": "gpii-app" + } + }, + // No outputPath, the output location is always c:/installer/ + "gpii-wix-installer": { + "type": "gpii.installer.artifact.githubRepoDownloader", + "options": { + "repo": "https://github.com/GPII/gpii-wix-installer", + "hash": "b98474d53e2e5aa667ed610bb19414e56790593a", + "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-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", + "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", + "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", + "outputPath": "staging/windows/resources/app/node_modules/gpii-universal/testData/defaultSettings/defaultSettings.json5" + } + } +} diff --git a/devTest.js b/devTest.js new file mode 100644 index 0000000..f00cd6c --- /dev/null +++ b/devTest.js @@ -0,0 +1,18 @@ +var fluid = require("infusion"); + +var gpii = fluid.registerNamespace("gpii"); + +require("./index.js"); + +//fluid.logObjectRenderChars = 1200000; + +var m = gpii.installer({}); +//console.log("#### buildFolder: ", m.options.buildFolder); + +//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..6978d89 --- /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("gpii-windows-installer", __dirname, require); + +var gpii = fluid.registerNamespace("gpii"); + +require("./src/main.js"); diff --git a/package.json b/package.json new file mode 100644 index 0000000..f4a88c3 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "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.14", + "dedupe-infusion": "1.0.0", + "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/provisioning/Build.ps1 b/provisioning/Build.ps1 new file mode 100644 index 0000000..77750b4 --- /dev/null +++ b/provisioning/Build.ps1 @@ -0,0 +1,78 @@ +<# + 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 +} + +$npmCmd = "npm" -f $env:SystemDrive + +## npm install pkg globally +Invoke-Command $npmCmd "install -g pkg" + +## 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 diff --git a/src/artifacts.js b/src/artifacts.js new file mode 100644 index 0000000..754e057 --- /dev/null +++ b/src/artifacts.js @@ -0,0 +1,179 @@ +/* +* 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"), + fse = require("fs-extra"), + 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"), + events: { + onPopulated: null, + onError: null + } +}); + +fluid.defaults("gpii.installer.artifact.downloader", { + gradeNames: "gpii.installer.artifact", + downloadUrl: null, + invokers: { + download: { + 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: ["unzip function called"] + } + }, + events: { + onUnzipped: null + }, + listeners: { + "onCreate.formatDownloadUrl": { + funcName: "gpii.installer.artifact.formatGithubDownloadUrl", + args: ["{that}"] + }, + "onCreate.download": { + func: "{that}.download", + args: ["{that}.options.downloadUrl", "{that}.options.defaultOutputPath", + "{that}.zipFile", + "{that}.events.onDownloaded", "{that}.events.onError" ], + priority:"after:onCreate.formatDownloadUrl" + }, + "onDownloaded.unzip": { + funcName: "gpii.installer.artifact.unzip", + args: ["{that}.zipFile", "{that}.options.defaultOutputPath", + "{that}.events.onPopulated", "{that}.events.onError"] + } + } +}); + + +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 outputFilePath = path.join(outputPath, outputFile); + + if (!fs.existsSync(outputPath)) fs.mkdirSync(outputPath); + if (fs.existsSync(outputFilePath)) fse.removeSync(outputFilePath); + + var outStream = fs.createWriteStream(outputFilePath); + + var req = request.get({ + uri: downloadUrl, + gzip: true + }); + + fluid.log("Downloading ", downloadUrl); + + var pipe = 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; + // This way we avoid the 'End-of-central-directory signature not found' + // error that sometimes we get when downloading zip files. + pipe.on("close", function () { + event.fire(outputFile); + }); + }); +}; + + +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. + * + * 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); + 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(outputPath, unzippedArtifactFolder), path.join(outputPath, finalArtifactFolder)); + // Remove the zipFile + fs.unlinkSync(zipFile); + event.fire(path.join(outputPath, finalArtifactFolder)); + } catch (err) { + fluid.log("Couldn't unzip", zipFile, "Error was:", err); + error.fire("Couldn't unzip " + zipFile); + } +}; diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..7cf5b2f --- /dev/null +++ b/src/main.js @@ -0,0 +1,341 @@ +/* +* 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 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("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: 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", + 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}"] + }, + shrinkSize: { + funcName: "gpii.installer.shrinkSize", + args: ["{that}"] + }, + copyOptionalArtifacts: { + funcName: "gpii.installer.copyOptionalArtifacts", + args: ["{that}"] + }, + runMsbuild: { + funcName: "gpii.installer.runMsbuild", + args: ["{that}"] + } + }, + events: { + onPopulatedArtifacts: null, + onBuildFolderReady: null, + onNpmInstallFinished: null, + onPackaged: null, + onWindowsServiceReady: null, + onShrunk: null, + onCopiedOptionalArtifacts: null, + onError: null + }, + listeners: { + "onCreate.populateArtifacts": { + func: "{that}.populateArtifacts", + args: "{that}.options.artifactsData" + }, + "onPopulatedArtifacts.logResult": { + funcName: "fluid.log", + args: ["Artifacts successfully populated"] + }, + "onPopulatedArtifacts.prepareBuildFolder": "{that}.prepareBuildFolder", + "onBuildFolderReady.runNpmInstall": "{that}.npmInstall", + "onNpmInstallFinished.logResult": { + funcName: "fluid.log", + args: ["npm install process succeeded"] + }, + "onNpmInstallFinished.runElectronPackager": "{that}.electronPackager", + "onPackaged.logResult": { + funcName: "fluid.log", + args: ["Morphic-App successfully packaged: ", "{arguments}.0"] + }, + "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.copyOptionalArtifacts": "{that}.copyOptionalArtifacts", + "onCopiedOptionalArtifacts.logResult": { + funcName: "fluid.log", + args: ["Copied optional artifacts"] + }, + "onCopiedOptionalArtifacts.runMsbuild": "{that}.runMsbuild", + "onError.logError": { + funcName: "fluid.fail", + args: "{arguments}.0" + } + } +}); + +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(artifactsData, function (artifactData, artifactName) { + fluid.log("Populating: ", artifactName); + var promise = fluid.promise(); + + 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) { + promise.reject(err); + }); + + sequence.push(promise); + }); + + fluid.promise.sequence(sequence).then(function (result) { + 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.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); + + // TODO: error handling + if (code) { + that.events.onError.fire("Couldn't finish npm install process - Check above for errors"); + } else { + that.events.onNpmInstallFinished.fire(); + } + }); +}; + +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); + }); +}; + +// 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}); + // 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 + // * 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")); + + that.events.onShrunk.fire(); + } + }); +}; + +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"); + 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(); + }); +}; diff --git a/src/windowsService.js b/src/windowsService.js new file mode 100644 index 0000000..98644a4 --- /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, "@gpii", "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(); +}