Skip to content

Commit e77769a

Browse files
committed
feat(nsis): disable 7-zip compression for specific static assets
Close #2628
1 parent b6580d8 commit e77769a

File tree

9 files changed

+119
-26
lines changed

9 files changed

+119
-26
lines changed

.idea/dictionaries/develar.xml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/electron-builder-lib/src/fileMatcher.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,11 @@ export function getMainFileMatchers(appDir: string, destination: string, macroEx
102102
const packager = platformPackager.info
103103
const buildResourceDir = path.resolve(packager.projectDir, packager.buildResourcesDir)
104104

105-
let matchers = packager.isPrepackedAppAsar ? null : getFileMatchers(packager.config, "files", appDir, destination, macroExpander, platformSpecificBuildOptions)
105+
let matchers = packager.isPrepackedAppAsar ? null : getFileMatchers(packager.config, "files", appDir, destination, {
106+
macroExpander,
107+
customBuildOptions: platformSpecificBuildOptions,
108+
outDir,
109+
})
106110
if (matchers == null) {
107111
matchers = [new FileMatcher(appDir, destination, macroExpander)]
108112
}
@@ -179,12 +183,18 @@ export function getMainFileMatchers(appDir: string, destination: string, macroEx
179183
return matchers
180184
}
181185

186+
export interface GetFileMatchersOptions {
187+
readonly macroExpander: (pattern: string) => string
188+
readonly customBuildOptions: PlatformSpecificBuildOptions
189+
readonly outDir: string
190+
}
191+
182192
/** @internal */
183-
export function getFileMatchers(config: Configuration, name: "files" | "extraFiles" | "extraResources" | "asarUnpack", defaultSrc: string, defaultDestination: string, macroExpander: (pattern: string) => string, customBuildOptions: PlatformSpecificBuildOptions): Array<FileMatcher> | null {
193+
export function getFileMatchers(config: Configuration, name: "files" | "extraFiles" | "extraResources" | "asarUnpack", defaultSrc: string, defaultDestination: string, options: GetFileMatchersOptions): Array<FileMatcher> | null {
184194
const globalPatterns: Array<string | FileSet> | string | null | undefined | FileSet = (config as any)[name]
185-
const platformSpecificPatterns: Array<string | FileSet> | string | null | undefined = (customBuildOptions as any)[name]
195+
const platformSpecificPatterns: Array<string | FileSet> | string | null | undefined = (options.customBuildOptions as any)[name]
186196

187-
const defaultMatcher = new FileMatcher(defaultSrc, defaultDestination, macroExpander)
197+
const defaultMatcher = new FileMatcher(defaultSrc, defaultDestination, options.macroExpander)
188198
const fileMatchers: Array<FileMatcher> = []
189199

190200
function addPatterns(patterns: Array<string | FileSet> | string | null | undefined | FileSet) {
@@ -210,7 +220,7 @@ export function getFileMatchers(config: Configuration, name: "files" | "extraFil
210220
else {
211221
const from = pattern.from == null ? defaultSrc : path.resolve(defaultSrc, pattern.from)
212222
const to = pattern.to == null ? defaultDestination : path.resolve(defaultDestination, pattern.to)
213-
fileMatchers.push(new FileMatcher(from, to, macroExpander, pattern.filter))
223+
fileMatchers.push(new FileMatcher(from, to, options.macroExpander, pattern.filter))
214224
}
215225
}
216226
}
@@ -223,6 +233,12 @@ export function getFileMatchers(config: Configuration, name: "files" | "extraFil
223233
fileMatchers.unshift(defaultMatcher)
224234
}
225235

236+
// we cannot exclude the whole out dir, because sometimes users want to use some file in the out dir in the patterns
237+
const relativeOutDir = defaultMatcher.normalizePattern(path.relative(defaultSrc, options.outDir))
238+
if (!relativeOutDir.startsWith(".")) {
239+
defaultMatcher.addPattern(`!${relativeOutDir}/*-unpacked{,/**/*}`)
240+
}
241+
226242
return fileMatchers.length === 0 ? null : fileMatchers
227243
}
228244

packages/electron-builder-lib/src/platformPackager.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { AppInfo } from "./appInfo"
1111
import { checkFileInArchive } from "./asar/asarFileChecker"
1212
import { AsarPackager } from "./asar/asarUtil"
1313
import { CompressionLevel, Platform, Target, TargetSpecificOptions } from "./core"
14-
import { copyFiles, FileMatcher, getFileMatchers, getMainFileMatchers } from "./fileMatcher"
14+
import { copyFiles, FileMatcher, getFileMatchers, GetFileMatchersOptions, getMainFileMatchers } from "./fileMatcher"
1515
import { createTransformer, isElectronCompileUsed } from "./fileTransformer"
1616
import { AfterPackContext, AsarOptions, Configuration, FileAssociation, PlatformSpecificBuildOptions } from "./index"
1717
import { Packager } from "./packager"
@@ -124,9 +124,9 @@ export abstract class PlatformPackager<DC extends PlatformSpecificBuildOptions>
124124
)
125125
}
126126

127-
private getExtraFileMatchers(isResources: boolean, appOutDir: string, macroExpander: (pattern: string) => string, customBuildOptions: DC): Array<FileMatcher> | null {
127+
private getExtraFileMatchers(isResources: boolean, appOutDir: string, options: GetFileMatchersOptions): Array<FileMatcher> | null {
128128
const base = isResources ? this.getResourcesDir(appOutDir) : (this.platform === Platform.MAC ? path.join(appOutDir, `${this.appInfo.productFilename}.app`, "Contents") : appOutDir)
129-
return getFileMatchers(this.config, isResources ? "extraResources" : "extraFiles", this.projectDir, base, macroExpander, customBuildOptions)
129+
return getFileMatchers(this.config, isResources ? "extraResources" : "extraFiles", this.projectDir, base, options)
130130
}
131131

132132
get electronDistMacOsAppName() {
@@ -177,9 +177,14 @@ export abstract class PlatformPackager<DC extends PlatformSpecificBuildOptions>
177177
}
178178
}
179179

180-
const extraResourceMatchers = this.getExtraFileMatchers(true, appOutDir, macroExpander, platformSpecificBuildOptions)
180+
const getFileMatchersOptions: GetFileMatchersOptions = {
181+
macroExpander,
182+
customBuildOptions: platformSpecificBuildOptions,
183+
outDir,
184+
}
185+
const extraResourceMatchers = this.getExtraFileMatchers(true, appOutDir, getFileMatchersOptions)
181186
computeParsedPatterns(extraResourceMatchers)
182-
const extraFileMatchers = this.getExtraFileMatchers(false, appOutDir, macroExpander, platformSpecificBuildOptions)
187+
const extraFileMatchers = this.getExtraFileMatchers(false, appOutDir, getFileMatchersOptions)
183188
computeParsedPatterns(extraFileMatchers)
184189

185190
const packContext: AfterPackContext = {
@@ -246,7 +251,11 @@ export abstract class PlatformPackager<DC extends PlatformSpecificBuildOptions>
246251
taskManager.addTask(BluebirdPromise.each(_computeFileSets(mainMatchers), it => copyAppFiles(it, this.info)))
247252
}
248253
else {
249-
const unpackPattern = getFileMatchers(config, "asarUnpack", appDir, defaultDestination, macroExpander, platformSpecificBuildOptions)
254+
const unpackPattern = getFileMatchers(config, "asarUnpack", appDir, defaultDestination, {
255+
macroExpander,
256+
customBuildOptions: platformSpecificBuildOptions,
257+
outDir,
258+
})
250259
const fileMatcher = unpackPattern == null ? null : unpackPattern[0]
251260
taskManager.addTask(_computeFileSets(mainMatchers)
252261
.then(fileSets => new AsarPackager(appDir, resourcePath, asarOptions, fileMatcher == null ? null : fileMatcher.createFilter()).pack(fileSets, this)))
@@ -332,6 +341,7 @@ export abstract class PlatformPackager<DC extends PlatformSpecificBuildOptions>
332341
if (pathParsed.dir.includes(".asar")) {
333342
// The path needs to be split to the part with an asar archive which acts like a directory and the part with
334343
// the path to main file itself. (e.g. path/arch.asar/dir/index.js -> path/arch.asar, dir/index.js)
344+
// noinspection TypeScriptValidateJSTypes
335345
const pathSplit: Array<string> = pathParsed.dir.split(path.sep)
336346
let partWithAsarIndex = 0
337347
pathSplit.some((pathPart: string, index: number) => {

packages/electron-builder-lib/src/targets/archive.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export interface ArchiveOptions {
6868
listFile?: string
6969

7070
dictSize?: number
71-
excluded?: Array<string>
71+
excluded?: Array<string> | null
7272

7373
// DEFAULT allows to disable custom logic and do not pass method switch at all
7474
method?: "Copy" | "LZMA" | "Deflate" | "DEFAULT"
@@ -149,7 +149,9 @@ export async function archive(format: string, outFile: string, dirToArchive: str
149149

150150
args.push(outFile, options.listFile == null ? (options.withoutDir ? "." : path.basename(dirToArchive)) : `@${options.listFile}`)
151151
if (options.excluded != null) {
152-
args.push(...options.excluded)
152+
for (const mask of options.excluded) {
153+
args.push(`-xr!${mask}`)
154+
}
153155
}
154156

155157
try {

packages/electron-builder-lib/src/targets/nsis/NsisTarget.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import BluebirdPromise from "bluebird-lst"
22
import { Arch, asArray, AsyncTaskManager, execWine, getPlatformIconFileName, InvalidConfigurationError, log, spawnAndWrite, use } from "builder-util"
33
import { PackageFileInfo, UUID } from "builder-util-runtime"
44
import { getBinFromGithub } from "builder-util/out/binDownload"
5-
import { statOrNull } from "builder-util/out/fs"
5+
import { statOrNull, walk } from "builder-util/out/fs"
66
import { hashFile } from "builder-util/out/hash"
77
import _debug from "debug"
88
import { readFile, stat, unlink } from "fs-extra-p"
@@ -42,7 +42,11 @@ export class NsisTarget extends Target {
4242

4343
this.packageHelper.refCount++
4444

45-
this.options = targetName === "portable" ? Object.create(null) : {...this.packager.config.nsis}
45+
this.options = targetName === "portable" ? Object.create(null) : {
46+
...this.packager.config.nsis,
47+
preCompressedFileExtensions: [".avi", ".mov", ".m4v", ".mp4", ".m4p", ".qt", ".mkv", ".webm", ".vmdk"],
48+
}
49+
4650
if (targetName !== "nsis") {
4751
Object.assign(this.options, (this.packager.config as any)[targetName === "nsis-web" ? "nsisWeb" : targetName])
4852
}
@@ -61,6 +65,11 @@ export class NsisTarget extends Target {
6165
return !this.isPortable && this.options.differentialPackage !== false
6266
}
6367

68+
private getPreCompressedFileExtensions(): Array<string> | null {
69+
const result = this.isWebInstaller ? null : this.options.preCompressedFileExtensions
70+
return result == null ? null : asArray(result).map(it => it.startsWith(".") ? it : `.${it}`)
71+
}
72+
6473
/** @private */
6574
async buildAppPackage(appOutDir: string, arch: Arch): Promise<PackageFileInfo> {
6675
const options = this.options
@@ -69,9 +78,11 @@ export class NsisTarget extends Target {
6978
const isBuildDifferentialAware = this.isBuildDifferentialAware
7079
const format = !isBuildDifferentialAware && options.useZip ? "zip" : "7z"
7180
const archiveFile = path.join(this.outDir, `${packager.appInfo.sanitizedName}-${packager.appInfo.version}-${Arch[arch]}.nsis.${format}`)
81+
const preCompressedFileExtensions = this.getPreCompressedFileExtensions()
7282
const archiveOptions: ArchiveOptions = {
7383
withoutDir: true,
7484
compression: packager.compression,
85+
excluded: preCompressedFileExtensions == null ? null : preCompressedFileExtensions.map(it => `*${it}`)
7586
}
7687

7788
const timer = time(`nsis package, ${Arch[arch]}`)
@@ -520,6 +531,20 @@ export class NsisTarget extends Target {
520531
return scriptGenerator.build() + originalScript
521532
}
522533

534+
const preCompressedFileExtensions = this.getPreCompressedFileExtensions()
535+
if (preCompressedFileExtensions != null) {
536+
for (const [arch, dir] of this.archs.entries()) {
537+
const preCompressedAssets = await walk(path.join(dir, "resources"), (file, stat) => stat.isDirectory() || preCompressedFileExtensions.some(it => file.endsWith(it)))
538+
if (preCompressedAssets.length !== 0) {
539+
const macro = new NsisScriptGenerator()
540+
for (const file of preCompressedAssets) {
541+
macro.file(`$INSTDIR\\${path.relative(dir, file).replace(/\//g, "\\")}`, file)
542+
}
543+
scriptGenerator.macro(`customFiles_${Arch[arch]}`, macro)
544+
}
545+
}
546+
}
547+
523548
const fileAssociations = packager.fileAssociations
524549
if (fileAssociations.length !== 0) {
525550
if (options.perMachine !== true && options.oneClick !== false) {

packages/electron-builder-lib/src/targets/nsis/nsisOptions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,12 @@ export interface NsisOptions extends CommonNsisOptions, CommonWindowsInstallerCo
146146
* @default true
147147
*/
148148
readonly packElevateHelper?: boolean
149+
150+
/**
151+
* The file extension of files that will be not compressed. Applicable only for `extraResources` and `extraFiles` files.
152+
* @default [".avi", ".mov", ".m4v", ".mp4", ".m4p", ".qt", ".mkv", ".webm", ".vmdk"]
153+
*/
154+
readonly preCompressedFileExtensions?: Array<string> | string | null
149155
}
150156

151157
/**

packages/electron-builder-lib/src/targets/nsis/nsisScriptGenerator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ export class NsisScriptGenerator {
2121
)
2222
}
2323

24-
file(outputName: string, file: string) {
25-
this.lines.push(`File "/oname=${outputName}" "${file}"`)
24+
file(outputName: string | null, file: string) {
25+
this.lines.push(`File${outputName == null ? "" : ` "/oname=${outputName}"`} "${file}"`)
2626
}
2727

2828
insertMacro(name: string, parameters: string) {

packages/electron-builder-lib/templates/nsis/include/extractAppPackage.nsh

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,15 @@
1010

1111
!ifdef APP_32
1212
${if} ${RunningX64}
13-
File /oname=$PLUGINSDIR\app-64.${COMPRESSION_METHOD} "${APP_64}"
13+
!insertmacro x64_app_files
1414
${else}
15-
StrCpy $packageArch "32"
16-
File /oname=$PLUGINSDIR\app-32.${COMPRESSION_METHOD} "${APP_32}"
15+
!insertmacro ia32_app_files
1716
${endIf}
1817
!else
19-
File /oname=$PLUGINSDIR\app-64.${COMPRESSION_METHOD} "${APP_64}"
18+
!insertmacro x64_app_files
2019
!endif
2120
!else
22-
StrCpy $packageArch "32"
23-
24-
File /oname=$PLUGINSDIR\app-32.${COMPRESSION_METHOD} "${APP_32}"
21+
!insertmacro ia32_app_files
2522
!endif
2623

2724
!ifdef COMPRESS
@@ -33,6 +30,26 @@
3330
!else
3431
!insertmacro extractUsing7za "$PLUGINSDIR\app-$packageArch.7z"
3532
!endif
33+
34+
# after decompression
35+
${if} $packageArch == "64"
36+
!ifmacrodef customFiles_x64
37+
!insertmacro customFiles_x64
38+
!endif
39+
${else}
40+
!ifmacrodef customFiles_ia32
41+
!insertmacro customFiles_ia32
42+
!endif
43+
${endIf}
44+
!macroend
45+
46+
!macro x64_app_files
47+
File /oname=$PLUGINSDIR\app-64.${COMPRESSION_METHOD} "${APP_64}"
48+
!macroend
49+
50+
!macro ia32_app_files
51+
StrCpy $packageArch "32"
52+
File /oname=$PLUGINSDIR\app-32.${COMPRESSION_METHOD} "${APP_32}"
3653
!macroend
3754

3855
!macro extractUsing7za FILE

test/src/windows/oneClickInstallerTest.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Arch, Platform } from "electron-builder"
2-
import { writeFile } from "fs-extra-p"
2+
import { copy, writeFile } from "fs-extra-p"
33
import * as path from "path"
44
import { assertThat } from "../helpers/fileAssert"
55
import { app, appThrows, assertPack, copyTestAsset, modifyPackageJson } from "../helpers/packTester"
@@ -122,7 +122,7 @@ test.ifNotCiMac("installerHeaderIcon", () => {
122122
)
123123
})
124124

125-
test.ifDevOrLinuxCi("custom include", () => assertPack("test-app-one", {targets: nsisTarget}, {
125+
test.ifDevOrLinuxCi("custom include", app({targets: nsisTarget}, {
126126
projectDirCreated: projectDir => copyTestAsset("installer.nsh", path.join(projectDir, "build", "installer.nsh")),
127127
packed: context => Promise.all([
128128
assertThat(path.join(context.projectDir, "build", "customHeader")).isFile(),
@@ -131,6 +131,21 @@ test.ifDevOrLinuxCi("custom include", () => assertPack("test-app-one", {targets:
131131
]),
132132
}))
133133

134+
test.skip("big file pack", app(
135+
{
136+
targets: nsisTarget,
137+
config: {
138+
extraResources: ["**/*.mov"],
139+
nsis: {
140+
differentialPackage: false,
141+
},
142+
},
143+
}, {
144+
projectDirCreated: async projectDir => {
145+
await copy("/Volumes/Pegasus/15.02.18.m4v", path.join(projectDir, "foo/bar/video.mov"))
146+
},
147+
}))
148+
134149
test.ifDevOrLinuxCi("custom script", app({targets: nsisTarget}, {
135150
projectDirCreated: projectDir => copyTestAsset("installer.nsi", path.join(projectDir, "build", "installer.nsi")),
136151
packed: context => assertThat(path.join(context.projectDir, "build", "customInstallerScript")).isFile(),

0 commit comments

Comments
 (0)