diff --git a/.idea/dictionaries/develar.xml b/.idea/dictionaries/develar.xml index eb129700c7e..1b86cb090f6 100644 --- a/.idea/dictionaries/develar.xml +++ b/.idea/dictionaries/develar.xml @@ -91,6 +91,7 @@ psmdcp rcedit readpass + regedit rels repos rimraf @@ -109,6 +110,7 @@ valuename veyor volid + winedlloverrides winstaller xamarin xenial diff --git a/docker/wine/Dockerfile b/docker/wine/Dockerfile index ebfba72c5ca..6967f5c7796 100644 --- a/docker/wine/Dockerfile +++ b/docker/wine/Dockerfile @@ -11,5 +11,6 @@ apt-get install -y --no-install-recommends wine1.8 mono-devel ca-certificates-mo apt-get clean && rm -rf /var/lib/apt/lists/* ENV WINEDEBUG -all,err+all +ENV WINEDLLOVERRIDES winemenubuilder.exe=d RUN wineboot --init || true \ No newline at end of file diff --git a/src/asarUtil.ts b/src/asarUtil.ts index 6f10830287f..f24c0dd807a 100644 --- a/src/asarUtil.ts +++ b/src/asarUtil.ts @@ -22,18 +22,20 @@ const MAX_FILE_REQUESTS = 32 const concurrency = {concurrency: MAX_FILE_REQUESTS} const NODE_MODULES_PATTERN = path.sep + "node_modules" + path.sep -function walk(dirPath: string, consumer: (file: string, stat: Stats) => void, filter: (file: string) => boolean, addRootToResult?: boolean): BluebirdPromise> { +export function walk(dirPath: string, consumer?: (file: string, stat: Stats) => void, filter?: (file: string) => boolean, addRootToResult?: boolean): BluebirdPromise> { return readdir(dirPath) .then(names => { return BluebirdPromise.map(names, name => { const filePath = dirPath + path.sep + name - if (!filter(filePath)) { + if (filter != null && !filter(filePath)) { return null } return lstat(filePath) .then((stat): any => { - consumer(filePath, stat) + if (consumer != null) { + consumer(filePath, stat) + } if (stat.isDirectory()) { return walk(filePath, consumer, filter, true) } diff --git a/templates/nsis/install.nsh b/templates/nsis/install.nsh index 6e11f041a16..d985a7d619b 100644 --- a/templates/nsis/install.nsh +++ b/templates/nsis/install.nsh @@ -1,3 +1,9 @@ +InitPluginsDir + +!ifdef HEADER_ICO + File /oname=$PLUGINSDIR\installerHeaderico.ico "${HEADER_ICO}" +!endif + ${IfNot} ${Silent} SetDetailsPrint none @@ -12,16 +18,23 @@ ${endif} !insertmacro CHECK_APP_RUNNING "install" -${if} $installMode == "all" - ReadRegStr $R0 HKEY_LOCAL_MACHINE "${UNINSTALL_REGISTRY_KEY}" UninstallString - ${if} $R0 != "" - ExecWait "$R0 /S" - ${endif} +ReadRegStr $R0 SHCTX "${UNINSTALL_REGISTRY_KEY}" UninstallString +${if} $R0 != "" + ExecWait "$R0 /S /KEEP_APP_DATA" ${endif} RMDir /r $INSTDIR SetOutPath $INSTDIR +SetCompress off +!ifdef APP_32 + File /oname=$PLUGINSDIR\app-32.7z "${APP_32}" +!endif +!ifdef APP_64 + File /oname=$PLUGINSDIR\app-64.7z "${APP_64}" +!endif +SetCompress "${COMPRESS}" + !ifdef APP_64 ${If} ${RunningX64} Nsis7z::Extract "$PLUGINSDIR\app-64.7z" diff --git a/templates/nsis/installer.nsi b/templates/nsis/installer.nsi index 6511c0beded..ad71b86fd5e 100644 --- a/templates/nsis/installer.nsi +++ b/templates/nsis/installer.nsi @@ -38,21 +38,6 @@ Function .onInit ${EndIf} !endif - InitPluginsDir - - SetCompress off - !ifdef APP_32 - File /oname=$PLUGINSDIR\app-32.7z "${APP_32}" - !endif - !ifdef APP_64 - File /oname=$PLUGINSDIR\app-64.7z "${APP_64}" - !endif - SetCompress "${COMPRESS}" - - !ifdef HEADER_ICO - File /oname=$PLUGINSDIR\installerHeaderico.ico "${HEADER_ICO}" - !endif - !ifmacrodef customInit !insertmacro customInit !endif diff --git a/templates/nsis/uninstaller.nsh b/templates/nsis/uninstaller.nsh index 2fcba4000b8..10629f60627 100644 --- a/templates/nsis/uninstaller.nsh +++ b/templates/nsis/uninstaller.nsh @@ -20,8 +20,6 @@ Function un.onInit FunctionEnd Section "un.install" - SetAutoClose true - !ifndef ONE_CLICK # for boring installer we check it here to show progress !insertmacro CHECK_APP_RUNNING "uninstall" @@ -44,6 +42,7 @@ Section "un.install" # delete the installed files RMDir /r $INSTDIR + ClearErrors ${GetParameters} $R0 ${GetOptions} $R0 "/KEEP_APP_DATA" $R1 ${If} ${Errors} @@ -63,4 +62,6 @@ Section "un.install" !ifmacrodef customUnInstall !insertmacro customUnInstall !endif + + Quit SectionEnd \ No newline at end of file diff --git a/test/src/helpers/expectedContents.ts b/test/src/helpers/expectedContents.ts index 805def2b891..3cbad92c465 100755 --- a/test/src/helpers/expectedContents.ts +++ b/test/src/helpers/expectedContents.ts @@ -1,3 +1,5 @@ +import pathSorter = require("path-sort") + //noinspection SpellCheckingInspection export const expectedLinuxContents = ["/", "/opt/", @@ -81,4 +83,87 @@ export const expectedWinContents = [ "TestApp.nuspec", "[Content_Types].xml", "_rels/.rels" -] \ No newline at end of file +] + +export const nsisPerMachineInstall = pathSorter([ + "Program Files/TestApp", + "Program Files/TestApp/blink_image_resources_200_percent.pak", + "Program Files/TestApp/content_resources_200_percent.pak", + "Program Files/TestApp/content_shell.pak", + "Program Files/TestApp/d3dcompiler_47.dll", + "Program Files/TestApp/ffmpeg.dll", + "Program Files/TestApp/icudtl.dat", + "Program Files/TestApp/libEGL.dll", + "Program Files/TestApp/libGLESv2.dll", + "Program Files/TestApp/LICENSE", + "Program Files/TestApp/LICENSES.chromium.html", + "Program Files/TestApp/natives_blob.bin", + "Program Files/TestApp/node.dll", + "Program Files/TestApp/snapshot_blob.bin", + "Program Files/TestApp/TestApp.exe", + "Program Files/TestApp/ui_resources_200_percent.pak", + "Program Files/TestApp/Uninstall TestApp.exe", + "Program Files/TestApp/views_resources_200_percent.pak", + "Program Files/TestApp/xinput1_3.dll", + "Program Files/TestApp/resources", + "Program Files/TestApp/resources/app.asar", + "Program Files/TestApp/resources/electron.asar", + "Program Files/TestApp/resources/foo.ico", + "Program Files/TestApp/locales", + "Program Files/TestApp/locales/am.pak", + "Program Files/TestApp/locales/ar.pak", + "Program Files/TestApp/locales/bg.pak", + "Program Files/TestApp/locales/bn.pak", + "Program Files/TestApp/locales/ca.pak", + "Program Files/TestApp/locales/cs.pak", + "Program Files/TestApp/locales/da.pak", + "Program Files/TestApp/locales/de.pak", + "Program Files/TestApp/locales/el.pak", + "Program Files/TestApp/locales/en-GB.pak", + "Program Files/TestApp/locales/en-US.pak", + "Program Files/TestApp/locales/es-419.pak", + "Program Files/TestApp/locales/es.pak", + "Program Files/TestApp/locales/et.pak", + "Program Files/TestApp/locales/fa.pak", + "Program Files/TestApp/locales/fake-bidi.pak", + "Program Files/TestApp/locales/fi.pak", + "Program Files/TestApp/locales/fil.pak", + "Program Files/TestApp/locales/fr.pak", + "Program Files/TestApp/locales/gu.pak", + "Program Files/TestApp/locales/he.pak", + "Program Files/TestApp/locales/hi.pak", + "Program Files/TestApp/locales/hr.pak", + "Program Files/TestApp/locales/hu.pak", + "Program Files/TestApp/locales/id.pak", + "Program Files/TestApp/locales/it.pak", + "Program Files/TestApp/locales/ja.pak", + "Program Files/TestApp/locales/kn.pak", + "Program Files/TestApp/locales/ko.pak", + "Program Files/TestApp/locales/lt.pak", + "Program Files/TestApp/locales/lv.pak", + "Program Files/TestApp/locales/ml.pak", + "Program Files/TestApp/locales/mr.pak", + "Program Files/TestApp/locales/ms.pak", + "Program Files/TestApp/locales/nb.pak", + "Program Files/TestApp/locales/nl.pak", + "Program Files/TestApp/locales/pl.pak", + "Program Files/TestApp/locales/pt-BR.pak", + "Program Files/TestApp/locales/pt-PT.pak", + "Program Files/TestApp/locales/ro.pak", + "Program Files/TestApp/locales/ru.pak", + "Program Files/TestApp/locales/sk.pak", + "Program Files/TestApp/locales/sl.pak", + "Program Files/TestApp/locales/sr.pak", + "Program Files/TestApp/locales/sv.pak", + "Program Files/TestApp/locales/sw.pak", + "Program Files/TestApp/locales/ta.pak", + "Program Files/TestApp/locales/te.pak", + "Program Files/TestApp/locales/th.pak", + "Program Files/TestApp/locales/tr.pak", + "Program Files/TestApp/locales/uk.pak", + "Program Files/TestApp/locales/vi.pak", + "Program Files/TestApp/locales/zh-CN.pak", + "Program Files/TestApp/locales/zh-TW.pak", + "users/Public/Desktop/TestApp.lnk", + "users/Public/Start Menu/Programs/TestApp.lnk" +]) \ No newline at end of file diff --git a/test/src/helpers/fileAssert.ts b/test/src/helpers/fileAssert.ts index 6ec043e8479..c0e43721957 100644 --- a/test/src/helpers/fileAssert.ts +++ b/test/src/helpers/fileAssert.ts @@ -89,7 +89,7 @@ class Assertions { } } -function prettyDiff(actual: any, expected: any): string { +export function prettyDiff(actual: any, expected: any): string { const diffJson2 = diffJson(expected, actual) const diff = diffJson2.map(part => { if (part.added) { diff --git a/test/src/helpers/wine.ts b/test/src/helpers/wine.ts new file mode 100644 index 00000000000..5802419e369 --- /dev/null +++ b/test/src/helpers/wine.ts @@ -0,0 +1,115 @@ +import { exec } from "out/util/util" +import { homedir } from "os" +import { emptyDir, readFile, writeFile, ensureDir } from "fs-extra-p" +import * as path from "path" +import { Promise as BluebirdPromise } from "bluebird" +import pathSorter = require("path-sort") +import { unlinkIfExists } from "out/util/util" + +//noinspection JSUnusedLocalSymbols +const __awaiter = require("out/util/awaiter") + +export class WineManager { + wineDir: string + private winePreparePromise: Promise | null + + private env: any + + userDir: string + + async prepare() { + if (this.env != null) { + return + } + + this.wineDir = path.join(homedir(), "wine-test") + + const env = process.env + const user = env.SUDO_USER || env.LOGNAME || env.USER || env.LNAME || env.USERNAME || (env.HOME === "/root" ? "root" : null) + if (user == null) { + throw new Error(`Cannot determinate user name: ${JSON.stringify(env, null, 2)}`) + } + + this.userDir = path.join(this.wineDir, "drive_c", "users", user) + + this.winePreparePromise = this.prepareWine(this.wineDir) + this.env = await this.winePreparePromise + } + + async exec(...args: Array) { + return exec("wine", args, {env: this.env}) + } + + async prepareWine(wineDir: string) { + await emptyDir(wineDir) + //noinspection SpellCheckingInspection + const env = Object.assign({}, process.env, { + WINEDLLOVERRIDES: "winemenubuilder.exe=d", + WINEPREFIX: wineDir + }) + + await exec("wineboot", ["--init"], {env: env}) + + // regedit often doesn't modify correctly + let systemReg = await readFile(path.join(wineDir, "system.reg"), "utf8") + systemReg = systemReg.replace('"CSDVersion"="Service Pack 3"', '"CSDVersion"=" "') + systemReg = systemReg.replace('"CurrentBuildNumber"="2600"', '"CurrentBuildNumber"="10240"') + systemReg = systemReg.replace('"CurrentVersion"="5.1"', '"CurrentVersion"="10.0"') + systemReg = systemReg.replace('"ProductName"="Microsoft Windows XP"', '"ProductName"="Microsoft Windows 10"') + systemReg = systemReg.replace('"CSDVersion"=dword:00000300', '"CSDVersion"=dword:00000000') + await writeFile(path.join(wineDir, "system.reg"), systemReg) + + // remove links to host OS + const desktopDir = path.join(this.userDir, "Desktop") + await BluebirdPromise.all([ + unlinkIfExists(desktopDir), + unlinkIfExists(path.join(this.userDir, "My Documents")), + unlinkIfExists(path.join(this.userDir, "My Music")), + unlinkIfExists(path.join(this.userDir, "My Pictures")), + unlinkIfExists(path.join(this.userDir, "My Videos")), + ]) + + await ensureDir(desktopDir) + return env + } +} + +enum ChangeType { + ADDED, REMOVED, NO_CHANGE +} + +export function diff(oldList: Array, newList: Array, rootDir: string) { + const delta: any = { + added: [], + deleted: [], + } + const deltaMap = new Map() + // const objHolder = new Set(oldList) + for (let item of oldList) { + deltaMap.set(item, ChangeType.REMOVED) + } + + for (let item of newList) { + // objHolder.add(item) + const d = deltaMap.get(item) + if (d === ChangeType.REMOVED) { + deltaMap.set(item, ChangeType.NO_CHANGE) + } + else { + deltaMap.set(item, ChangeType.ADDED) + } + } + + for (let [item, changeType] of deltaMap.entries()) { + if (changeType === ChangeType.REMOVED) { + delta.deleted.push(item.substring(rootDir.length + 1)) + } + else if (changeType === ChangeType.ADDED) { + delta.added.push(item.substring(rootDir.length + 1)) + } + } + + delta.added = pathSorter(delta.added) + delta.deleted = pathSorter(delta.deleted) + return delta +} \ No newline at end of file diff --git a/test/src/nsisTest.ts b/test/src/nsisTest.ts index 72695c35640..0564891e7c3 100644 --- a/test/src/nsisTest.ts +++ b/test/src/nsisTest.ts @@ -1,10 +1,14 @@ import { Platform, Arch } from "out" import test from "./helpers/avaEx" import { assertPack, getTestAsset, app } from "./helpers/packTester" -import { copy } from "fs-extra-p" +import { copy, outputFile } from "fs-extra-p" import * as path from "path" import { Promise as BluebirdPromise } from "bluebird" import { assertThat } from "./helpers/fileAssert" +import { extractFile } from "asar-electron-builder" +import { walk } from "out/asarUtil" +import { nsisPerMachineInstall } from "./helpers/expectedContents" +import { WineManager, diff } from "./helpers/wine" //noinspection JSUnusedLocalSymbols const __awaiter = require("out/util/awaiter") @@ -13,9 +17,11 @@ const nsisTarget = Platform.WINDOWS.createTarget(["nsis"]) test("one-click", app({targets: nsisTarget}, {useTempDir: true, signed: true})) test.ifDevOrLinuxCi("perMachine, no run after finish", app({ - targets: Platform.WINDOWS.createTarget(["nsis"], Arch.ia32, Arch.x64), + targets: Platform.WINDOWS.createTarget(["nsis"], Arch.ia32), devMetadata: { build: { + // wine creates incorrect filenames and registry entries for unicode, so, we use ASCII + productName: "TestApp", fileAssociations: [ { ext: "foo", @@ -27,12 +33,56 @@ test.ifDevOrLinuxCi("perMachine, no run after finish", app({ runAfterFinish: false, }, } - } + }, }, { projectDirCreated: projectDir => { let headerIconPath = path.join(projectDir, "build", "foo.ico") return copy(getTestAsset("headerIcon.ico"), headerIconPath) }, + packed: async (projectDir, outDir) => { + if (process.env.CI != null) { + return + } + + const wine = new WineManager() + await wine.prepare() + const driveC = path.join(wine.wineDir, "drive_c") + const driveCWindows = path.join(wine.wineDir, "drive_c", "windows") + const perUserTempDir = path.join(wine.userDir, "Temp") + const walkFilter = (it: string) => { + return it !== driveCWindows && it !== perUserTempDir + } + + function listFiles() { + return walk(driveC, null, walkFilter) + } + + let fsBefore = await listFiles() + + await wine.exec(path.join(outDir, "TestApp Setup 1.1.0.exe"), "/S") + + const appAsar = path.join(driveC, "Program Files", "TestApp", "resources", "app.asar") + assertThat(JSON.parse(extractFile(appAsar, "package.json").toString())).hasProperties({ + name: "TestApp" + }) + + let fsAfter = await listFiles() + + let fsChanges = diff(fsBefore, fsAfter, driveC) + assertThat(fsChanges.added).isEqualTo(nsisPerMachineInstall) + assertThat(fsChanges.deleted).isEqualTo([]) + + // run installer again to test uninstall + const appDataFile = path.join(wine.userDir, "Application Data", "TestApp", "doNotDeleteMe") + await outputFile(appDataFile, "app data must be not removed") + fsBefore = await listFiles() + await wine.exec(path.join(outDir, "TestApp Setup 1.1.0.exe")) + fsAfter = await listFiles() + + fsChanges = diff(fsBefore, fsAfter, driveC) + assertThat(fsChanges.added).isEqualTo([]) + assertThat(fsChanges.deleted).isEqualTo([]) + }, })) test.ifNotCiOsx("boring", app({ @@ -95,7 +145,7 @@ test.ifNotCiOsx("boring, MUI_HEADER", () => { test.ifNotCiOsx("boring, MUI_HEADER as option", () => { let installerHeaderPath: string | null = null return assertPack("test-app-one", { - targets: nsisTarget, + targets: Platform.WINDOWS.createTarget(["nsis"], Arch.ia32, Arch.x64), devMetadata: { build: { nsis: {