Skip to content

Commit 308438f

Browse files
ha-Ddevelar
authored andcommitted
feat: support from/to paths in file patterns for extraFiles and extraResources
Closes #650
1 parent 2a24a3f commit 308438f

File tree

2 files changed

+170
-60
lines changed

2 files changed

+170
-60
lines changed

docs/Options.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,5 +225,22 @@ May be specified in the platform options (e.g. in the `build.mac`).
225225
Remember that `!doNotCopyMe/**/*` would match the files *in* the `doNotCopyMe` directory, but not the directory itself, so the [empty directory](https://github.com/gulpjs/gulp/issues/165#issuecomment-32613179) would be created.
226226
Solution — use macro `${/*}`, e.g. `!doNotCopyMe${/*}`.
227227

228+
## Source and Destination Directories
229+
You may also specify custom source and destination directories by using JSON objects instead of simple glob patterns.
230+
Note this only works for `extraFiles` and `extraResources`.
231+
```js
232+
[
233+
{
234+
"from": "path/to/source",
235+
"to": "path/to/destination",
236+
"filter": ["**/*", "!foo/*.js"]
237+
}
238+
]
239+
```
240+
If `from` is given as a relative path, it is relative to the project directory.
241+
If `to` is given as a relative path, it is relative to the app's content directory for `extraFiles` and the app's resource directory for `extraResources`.
242+
243+
You can you `${os}` and `${arch}` in the `from` and `to` fields as well.
244+
228245
# Build Version Management
229246
`CFBundleVersion` (MacOS) and `FileVersion` (Windows) will be set automatically to `version`.`build_number` on CI server (Travis, AppVeyor and CircleCI supported).

src/platformPackager.ts

Lines changed: 153 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,85 @@ export abstract class TargetEx extends Target {
8383
abstract async build(appOutDir: string, arch: Arch): Promise<any>
8484
}
8585

86+
export interface FilePattern {
87+
from?: string
88+
to?: string
89+
filter?: Array<string> | string
90+
}
91+
92+
export interface FileMatchOptions {
93+
arch: string,
94+
os: string
95+
}
96+
97+
export class FileMatcher {
98+
readonly from: string
99+
readonly to: string
100+
readonly options: FileMatchOptions
101+
102+
readonly patterns: Array<string>
103+
104+
constructor(from: string, to: string, options: FileMatchOptions, patterns?: Array<string> | string | n) {
105+
this.options = options
106+
this.patterns = []
107+
108+
this.from = this.expandPattern(from)
109+
this.to = this.expandPattern(to)
110+
111+
if (patterns != null && !Array.isArray(patterns)) {
112+
this.patterns = [patterns]
113+
}
114+
else if (patterns != null) {
115+
this.patterns = patterns
116+
}
117+
}
118+
119+
addPattern(pattern: string) {
120+
this.patterns.push(pattern)
121+
}
122+
123+
isEmpty() {
124+
return this.patterns.length === 0
125+
}
126+
127+
getParsedPatterns(fromDir?: string): Array<Minimatch> {
128+
const minimatchOptions = {}
129+
130+
const parsedPatterns: Array<Minimatch> = []
131+
const pathDifference = fromDir ? path.relative(fromDir, this.from) : null
132+
133+
for (let i = 0; i < this.patterns.length; i++) {
134+
let expandedPattern = this.expandPattern(this.patterns[i])
135+
136+
if (pathDifference) {
137+
expandedPattern = path.join(pathDifference, expandedPattern)
138+
}
139+
140+
const parsedPattern = new Minimatch(expandedPattern, minimatchOptions)
141+
parsedPatterns.push(parsedPattern)
142+
143+
if (!hasMagic(parsedPattern)) {
144+
// https://github.com/electron-userland/electron-builder/issues/545
145+
// add **/*
146+
parsedPatterns.push(new Minimatch(`${expandedPattern}/*/**`, minimatchOptions))
147+
}
148+
}
149+
150+
return parsedPatterns
151+
}
152+
153+
createFilter(ignoreFiles?: Set<string>, rawFilter?: (file: string) => boolean, excludePatterns?: Array<Minimatch> | n): (file: string) => boolean {
154+
return createFilter(this.from, this.getParsedPatterns(), ignoreFiles, rawFilter, excludePatterns)
155+
}
156+
157+
private expandPattern(pattern: string): string {
158+
return pattern
159+
.replace(/\$\{arch}/g, this.options.arch)
160+
.replace(/\$\{os}/g, this.options.os)
161+
.replace(/\$\{\/\*}/g, "{,/**/*,/**/.*}")
162+
}
163+
}
164+
86165
export abstract class PlatformPackager<DC extends PlatformSpecificBuildOptions> {
87166
readonly options: PackagerOptions
88167

@@ -156,16 +235,22 @@ export abstract class PlatformPackager<DC extends PlatformSpecificBuildOptions>
156235

157236
abstract pack(outDir: string, arch: Arch, targets: Array<Target>, postAsyncTasks: Array<Promise<any>>): Promise<any>
158237

159-
private getExtraFilePatterns(isResources: boolean, arch: Arch, customBuildOptions: DC): Array<Minimatch> | null {
160-
const patterns = this.getFilePatterns(isResources ? "extraResources" : "extraFiles", customBuildOptions)
161-
return patterns == null || patterns.length === 0 ? null : this.getParsedPatterns(patterns, arch)
238+
private getExtraFilePatterns(isResources: boolean, appOutDir: string, fileMatchOptions: FileMatchOptions, customBuildOptions: DC): Array<FileMatcher> | n {
239+
const base = isResources ? this.getResourcesDir(appOutDir) : (this.platform === Platform.MAC ? path.join(appOutDir, `${this.appInfo.productFilename}.app`, "Contents") : appOutDir)
240+
return this.getFilePatterns(isResources ? "extraResources" : "extraFiles", this.projectDir, base, true, fileMatchOptions, customBuildOptions)
162241
}
163242

164243
protected async doPack(options: ElectronPackagerOptions, outDir: string, appOutDir: string, platformName: string, arch: Arch, platformSpecificBuildOptions: DC) {
165244
const asarOptions = this.computeAsarOptions(platformSpecificBuildOptions)
245+
const fileMatchOptions: FileMatchOptions = {
246+
arch: Arch[arch],
247+
os: this.platform.buildConfigurationKey
248+
}
249+
250+
const extraResourcePatterns = this.getExtraFilePatterns(true, appOutDir, fileMatchOptions, platformSpecificBuildOptions)
251+
const extraFilePatterns = this.getExtraFilePatterns(false, appOutDir, fileMatchOptions, platformSpecificBuildOptions)
166252

167-
const extraResourcePatterns = this.getExtraFilePatterns(true, arch, platformSpecificBuildOptions)
168-
const extraFilePatterns = this.getExtraFilePatterns(false, arch, platformSpecificBuildOptions)
253+
const resourcesPath = this.platform === Platform.MAC ? path.join(appOutDir, "Electron.app", "Contents", "Resources") : path.join(appOutDir, "resources")
169254

170255
const p = pack(options, appOutDir, platformName, Arch[arch], this.info.electronVersion, async() => {
171256
const ignoreFiles = new Set([path.resolve(this.info.appDir, outDir), path.resolve(this.info.appDir, this.buildResourcesDir)])
@@ -176,11 +261,13 @@ export abstract class PlatformPackager<DC extends PlatformSpecificBuildOptions>
176261
}
177262
}
178263

179-
let patterns = this.getFilePatterns("files", platformSpecificBuildOptions)
180-
if (patterns == null || patterns.length === 0) {
181-
patterns = ["**/*"]
264+
let patterns = this.getFilePatterns("files", this.info.appDir, path.join(resourcesPath, "app"), false, fileMatchOptions, platformSpecificBuildOptions)
265+
let defaultMatcher = patterns != null ? patterns[0] : new FileMatcher(this.info.appDir, path.join(resourcesPath, "app"), fileMatchOptions)
266+
267+
if (defaultMatcher.isEmpty()) {
268+
defaultMatcher.addPattern("**/*")
182269
}
183-
patterns.push("!**/node_modules/*/{README.md,README,readme.md,readme,test}")
270+
defaultMatcher.addPattern("!**/node_modules/*/{README.md,README,readme.md,readme,test}")
184271

185272
let rawFilter: any = null
186273
const deprecatedIgnore = (<any>this.devMetadata.build).ignore
@@ -194,24 +281,21 @@ export abstract class PlatformPackager<DC extends PlatformSpecificBuildOptions>
194281
rawFilter = deprecatedUserIgnoreFilter(options, this.info.appDir)
195282
}
196283

197-
const filePatterns = this.getParsedPatterns(patterns, arch)
198-
let excludePatterns: Array<Minimatch> | null = null
199-
if (!this.info.isTwoPackageJsonProjectLayoutUsed) {
200-
if (extraResourcePatterns != null) {
201-
excludePatterns = extraResourcePatterns
284+
let excludePatterns: Array<Minimatch> = []
285+
if (extraResourcePatterns != null) {
286+
for (let i = 0; i < extraResourcePatterns.length; i++) {
287+
const patterns = extraResourcePatterns[i].getParsedPatterns(this.info.projectDir)
288+
excludePatterns = excludePatterns.concat(patterns)
202289
}
203-
if (extraFilePatterns != null) {
204-
if (excludePatterns == null) {
205-
excludePatterns = extraFilePatterns
206-
}
207-
else {
208-
excludePatterns = excludePatterns.concat(extraFilePatterns)
209-
}
290+
}
291+
if (extraFilePatterns != null) {
292+
for (let i = 0; i < extraFilePatterns.length; i++) {
293+
const patterns = extraFilePatterns[i].getParsedPatterns(this.info.projectDir)
294+
excludePatterns = excludePatterns.concat(patterns)
210295
}
211296
}
212297

213-
const resourcesPath = this.platform === Platform.MAC ? path.join(appOutDir, "Electron.app", "Contents", "Resources") : path.join(appOutDir, "resources")
214-
const filter = createFilter(this.info.appDir, filePatterns, ignoreFiles, rawFilter, excludePatterns)
298+
const filter = defaultMatcher.createFilter(ignoreFiles, rawFilter, excludePatterns.length ? excludePatterns : null)
215299
const promise = asarOptions == null ?
216300
copyFiltered(this.info.appDir, path.join(resourcesPath, "app"), filter, this.platform === Platform.WINDOWS)
217301
: createAsarArchive(this.info.appDir, resourcesPath, asarOptions, filter)
@@ -227,8 +311,8 @@ export abstract class PlatformPackager<DC extends PlatformSpecificBuildOptions>
227311
})
228312
await task(`Packaging for platform ${platformName} ${Arch[arch]} using electron ${this.info.electronVersion} to ${path.relative(this.projectDir, appOutDir)}`, p)
229313

230-
await this.doCopyExtraFiles(true, appOutDir, extraResourcePatterns)
231-
await this.doCopyExtraFiles(false, appOutDir, extraFilePatterns)
314+
await this.doCopyExtraFiles(extraResourcePatterns)
315+
await this.doCopyExtraFiles(extraFilePatterns)
232316

233317
const afterPack = this.devMetadata.build.afterPack
234318
if (afterPack != null) {
@@ -294,54 +378,63 @@ export abstract class PlatformPackager<DC extends PlatformSpecificBuildOptions>
294378
})
295379
}
296380

297-
private expandPattern(pattern: string, arch: Arch): string {
298-
return pattern
299-
.replace(/\$\{arch}/g, Arch[arch])
300-
.replace(/\$\{os}/g, this.platform.buildConfigurationKey)
301-
.replace(/\$\{\/\*}/g, "{,/**/*,/**/.*}")
302-
}
303-
304-
private doCopyExtraFiles(isResources: boolean, appOutDir: string, patterns: Array<Minimatch> | null): Promise<any> {
305-
const base = isResources ? this.getResourcesDir(appOutDir) : (this.platform === Platform.MAC ? path.join(appOutDir, `${this.appInfo.productFilename}.app`, "Contents") : appOutDir)
381+
private doCopyExtraFiles(patterns: Array<FileMatcher> | n): Promise<any> {
306382
if (patterns == null || patterns.length === 0) {
307383
return BluebirdPromise.resolve()
308384
}
309385
else {
310-
return copyFiltered(this.projectDir, base, createFilter(this.projectDir, patterns), this.platform === Platform.WINDOWS)
386+
const promises: Array<Promise<any>> = []
387+
for (let i = 0; i < patterns.length; i++) {
388+
if (patterns[i].isEmpty()) {
389+
patterns[i].addPattern("**/*")
390+
}
391+
promises.push(copyFiltered(patterns[i].from, patterns[i].to, patterns[i].createFilter(), this.platform === Platform.WINDOWS))
392+
}
393+
return BluebirdPromise.all(promises)
311394
}
312395
}
313396

314-
private getParsedPatterns(patterns: Array<string>, arch: Arch): Array<Minimatch> {
315-
const minimatchOptions = {}
316-
const parsedPatterns: Array<Minimatch> = []
317-
for (let i = 0; i < patterns.length; i++) {
318-
const pattern = this.expandPattern(patterns[i], arch)
319-
const parsedPattern = new Minimatch(pattern, minimatchOptions)
320-
parsedPatterns.push(parsedPattern)
321-
if (!hasMagic(parsedPattern)) {
322-
// https://github.com/electron-userland/electron-builder/issues/545
323-
// add **/*
324-
parsedPatterns.push(new Minimatch(`${pattern}/*/**`, minimatchOptions))
397+
private getFilePatterns(name: "files" | "extraFiles" | "extraResources", defaultSrc: string, defaultDest: string, allowAdvancedMatching: boolean, fileMatchOptions: FileMatchOptions, customBuildOptions: DC): Array<FileMatcher> | n {
398+
let globalPatterns: Array<string | FilePattern> | string | n = (<any>this.devMetadata.build)[name]
399+
let platformSpecificPatterns: Array<string | FilePattern> | string | n = (<any>customBuildOptions)[name]
400+
401+
const defaultMatcher = new FileMatcher(defaultSrc, defaultDest, fileMatchOptions)
402+
const fileMatchers: Array<FileMatcher> = []
403+
404+
function addPatterns(patterns: Array<string | FilePattern> | string | n) {
405+
if (patterns == null) {
406+
return
407+
}
408+
else if (!Array.isArray(patterns)) {
409+
defaultMatcher.addPattern(patterns)
410+
return
411+
}
412+
413+
for (let i = 0; i < patterns.length; i++) {
414+
const pattern = patterns[i]
415+
if (typeof pattern === "string") {
416+
defaultMatcher.addPattern(pattern)
417+
}
418+
else if (allowAdvancedMatching) {
419+
const from = pattern.from ? (path.isAbsolute(pattern.from) ? pattern.from : path.join(defaultSrc, pattern.from)) : defaultSrc
420+
const to = pattern.to ? (path.isAbsolute(pattern.to) ? pattern.to : path.join(defaultDest, pattern.to)) : defaultDest
421+
fileMatchers.push(new FileMatcher(from, to, fileMatchOptions, pattern.filter))
422+
}
423+
else {
424+
throw new Error(`Advanced file copying not supported for "${name}"`)
425+
}
325426
}
326427
}
327428

328-
return parsedPatterns
329-
}
429+
addPatterns(globalPatterns)
430+
addPatterns(platformSpecificPatterns)
330431

331-
private getFilePatterns(name: "files" | "extraFiles" | "extraResources", customBuildOptions: DC): Array<string> | n {
332-
let patterns: Array<string> | string | n = (<any>this.devMetadata.build)[name]
333-
if (patterns != null && !Array.isArray(patterns)) {
334-
patterns = [patterns]
432+
if (!defaultMatcher.isEmpty()) {
433+
// Default matcher should be first in the array
434+
fileMatchers.unshift(defaultMatcher)
335435
}
336436

337-
let platformSpecificPatterns: Array<string> | string | n = (<any>customBuildOptions)[name]
338-
if (platformSpecificPatterns != null) {
339-
if (!Array.isArray(platformSpecificPatterns)) {
340-
platformSpecificPatterns = [platformSpecificPatterns]
341-
}
342-
return patterns == null ? platformSpecificPatterns : Array.from(new Set(patterns.concat(platformSpecificPatterns)))
343-
}
344-
return patterns
437+
return fileMatchers.length ? fileMatchers : null
345438
}
346439

347440
private getResourcesDir(appOutDir: string): string {

0 commit comments

Comments
 (0)