Skip to content

Commit b373275

Browse files
committed
fix(AppImage): restore appimaged compatibility (AppImage 2)
1 parent 65fcb71 commit b373275

10 files changed

Lines changed: 76 additions & 40 deletions

File tree

.idea/dictionaries/develar.xml

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

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"///": "Please see https://github.com/electron-userland/electron-builder/blob/master/CONTRIBUTING.md#run-test-using-cli how to run particular test instead full (and very slow) run",
1111
"test": "node ./test/out/helpers/runTests.js skipArtifactPublisher ALL_TESTS=isCi",
1212
"test-all": "yarn pretest && node ./test/out/helpers/runTests.js",
13-
"test-linux": "docker run --rm -ti -v ${PWD}:/project -v ${PWD##*/}-node-modules:/project/node_modules -v ~/.electron:/root/.electron electronuserland/builder:wine /bin/bash -c \"yarn && yarn test\"",
13+
"test-linux": "docker run --rm -ti --env TEST_FILES='linuxPackagerTest' -v ${PWD}:/project -v ${PWD##*/}-node-modules:/project/node_modules -v ~/.electron:/root/.electron electronuserland/builder:wine /bin/bash -c \"yarn && yarn test\"",
1414
"whitespace": "whitespace 'src/**/*.ts'",
1515
"docker-images": "docker/build.sh",
1616
"update-deps": "npm-check-updates -a -x gitbook-plugin-github && node ./scripts/update-deps.js",
@@ -31,7 +31,7 @@
3131
"7zip-bin": "^2.2.4",
3232
"archiver": "^2.0.3",
3333
"async-exit-hook": "^2.0.1",
34-
"aws-sdk": "^2.126.0",
34+
"aws-sdk": "^2.127.0",
3535
"bluebird-lst": "^1.0.3",
3636
"chalk": "^2.1.0",
3737
"chromium-pickle-js": "^0.2.0",

packages/builder-util/src/fs.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,12 @@ export class FileCopier {
211211
isUseHardLink: boolean
212212

213213
constructor(private readonly isUseHardLinkFunction?: (file: string) => boolean, private readonly transformer?: FileTransformer | null) {
214-
this.isUseHardLink = _isUseHardLink && isUseHardLinkFunction !== DO_NOT_USE_HARD_LINKS
214+
if (isUseHardLinkFunction === USE_HARD_LINKS) {
215+
this.isUseHardLink = true
216+
}
217+
else {
218+
this.isUseHardLink = _isUseHardLink && isUseHardLinkFunction !== DO_NOT_USE_HARD_LINKS
219+
}
215220
}
216221

217222
async copy(src: string, dest: string, stat: Stats | undefined) {
@@ -228,7 +233,8 @@ export class FileCopier {
228233
}
229234
}
230235
}
231-
await copyOrLinkFile(src, dest, stat, (!this.isUseHardLink || this.isUseHardLinkFunction == null) ? this.isUseHardLink : this.isUseHardLinkFunction(dest), this.isUseHardLink ? () => {
236+
const isUseHardLink = (!this.isUseHardLink || this.isUseHardLinkFunction == null) ? this.isUseHardLink : this.isUseHardLinkFunction(dest)
237+
await copyOrLinkFile(src, dest, stat, isUseHardLink, isUseHardLink ? () => {
232238
// files are copied concurrently, so, we must not check here currentIsUseHardLink — our code can be executed after that other handler will set currentIsUseHardLink to false
233239
if (this.isUseHardLink) {
234240
this.isUseHardLink = false
@@ -241,20 +247,26 @@ export class FileCopier {
241247
}
242248
}
243249

250+
export interface CopyDirOptions {
251+
filter?: Filter | null
252+
transformer?: FileTransformer | null
253+
isUseHardLink?: (file: string) => boolean
254+
}
255+
244256
/**
245257
* Empty directories is never created.
246258
* Hard links is used if supported and allowed.
247259
*/
248-
export function copyDir(src: string, destination: string, filter?: Filter | null, transformer?: FileTransformer | null, isUseHardLink?: (file: string) => boolean): Promise<any> {
249-
const fileCopier = new FileCopier(isUseHardLink, transformer)
260+
export function copyDir(src: string, destination: string, options: CopyDirOptions = {}): Promise<any> {
261+
const fileCopier = new FileCopier(options.isUseHardLink, options.transformer)
250262

251263
if (debug.enabled) {
252264
debug(`Copying ${src} to ${destination}${fileCopier.isUseHardLink ? " using hard links" : ""}`)
253265
}
254266

255267
const createdSourceDirs = new Set<string>()
256268
const links: Array<Link> = []
257-
return walk(src, filter, {
269+
return walk(src, options.filter, {
258270
consume: async (file, stat, parent) => {
259271
if (!stat.isFile() && !stat.isSymbolicLink()) {
260272
return
@@ -278,6 +290,7 @@ export function copyDir(src: string, destination: string, filter?: Filter | null
278290
}
279291

280292
export const DO_NOT_USE_HARD_LINKS = (file: string) => false
293+
export const USE_HARD_LINKS = (file: string) => true
281294

282295
export interface Link {
283296
readonly link: string,

packages/electron-builder/src/fileMatcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,6 @@ export function copyFiles(matchers: Array<FileMatcher> | null): Promise<any> {
253253
if (debug.enabled) {
254254
debug(`Copying files using pattern: ${matcher}`)
255255
}
256-
return await copyDir(matcher.from, matcher.to, matcher.createFilter())
256+
return await copyDir(matcher.from, matcher.to, {filter: matcher.createFilter()})
257257
})
258258
}

packages/electron-builder/src/packager/dirPackager.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ async function unpack(packager: PlatformPackager<any>, out: string, platform: st
6161
const destination = packager.getElectronDestinationDir(out)
6262
log(`Copying Electron from "${source}" to "${destination}"`)
6363
await emptyDir(out)
64-
await copyDir(source, destination, null, null, DO_NOT_USE_HARD_LINKS)
64+
await copyDir(source, destination, {
65+
isUseHardLink: DO_NOT_USE_HARD_LINKS,
66+
})
6567
}
6668

6769
if (platform === "linux") {

packages/electron-builder/src/targets/appImage.ts

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import BluebirdPromise from "bluebird-lst"
22
import { Arch, exec, log, debug } from "builder-util"
33
import { UUID } from "builder-util-runtime"
44
import { getBinFromGithub } from "builder-util/out/binDownload"
5-
import { unlinkIfExists, copyOrLinkFile } from "builder-util/out/fs"
5+
import { unlinkIfExists, copyOrLinkFile, copyDir, USE_HARD_LINKS } from "builder-util/out/fs"
66
import * as ejs from "ejs"
7-
import { emptyDir, ensureDir, readFile, remove, writeFile } from "fs-extra-p"
7+
import { emptyDir, ensureDir, readFile, remove, symlink, writeFile } from "fs-extra-p"
88
import { Lazy } from "lazy-val"
99
import * as path from "path"
1010
import { Target } from "../core"
@@ -45,24 +45,10 @@ export default class AppImageTarget extends Target {
4545
const stageDir = path.join(this.outDir, `__appimage-${Arch[arch]}`)
4646
const appInStageDir = path.join(stageDir, "app")
4747
await emptyDir(stageDir)
48-
await copyDirUsingHardLinks(appOutDir, appInStageDir, true)
49-
50-
const iconNames = await BluebirdPromise.map(this.helper.icons, it => {
51-
let filename = `icon-${it.size}.png`
52-
if (it.file === this.helper.maxIconPath) {
53-
// largest icon as package icon
54-
filename = `${this.packager.executableName}.png`
55-
}
56-
return copyOrLinkFile(it.file, path.join(stageDir, filename), null, true)
57-
.then(() => ({filename, size: it.size}))
58-
})
48+
await copyDirUsingHardLinks(appOutDir, appInStageDir)
5949

6050
const resourceName = `appimagekit-${this.packager.executableName}`
61-
62-
let installIcons = ""
63-
for (const icon of iconNames) {
64-
installIcons += `xdg-icon-resource install --noupdate --context apps --size ${icon.size} "$APPDIR/${icon.filename}" "${resourceName}"\n`
65-
}
51+
const installIcons = await this.copyIcons(stageDir, resourceName)
6652

6753
const finalDesktopFilename = `${this.packager.executableName}.desktop`
6854
await BluebirdPromise.all([
@@ -88,7 +74,9 @@ export default class AppImageTarget extends Target {
8874
const vendorDir = await getBinFromGithub("appimage", "9.0.1", "mcme+7/krXSYb5C+6BpSt9qgajFYpn9dI1rjxzSW3YB5R/KrGYYrpZbVflEMG6pM7k9CL52poiOpGLBDG/jW3Q==")
8975

9076
if (arch === Arch.x64 || arch === Arch.ia32) {
91-
await copyDirUsingHardLinks(path.join(vendorDir, "lib", arch === Arch.x64 ? "x86_64-linux-gnu" : "i386-linux-gnu"), path.join(stageDir, "usr/lib"), false)
77+
await copyDir(path.join(vendorDir, "lib", arch === Arch.x64 ? "x86_64-linux-gnu" : "i386-linux-gnu"), path.join(stageDir, "usr/lib"), {
78+
isUseHardLink: USE_HARD_LINKS,
79+
})
9280
}
9381

9482
if (this.packager.packagerOptions.effectiveOptionComputed != null && await this.packager.packagerOptions.effectiveOptionComputed({desktop: await this.desktopEntry.value})) {
@@ -116,15 +104,41 @@ export default class AppImageTarget extends Target {
116104
}
117105
packager.dispatchArtifactCreated(resultFile, this, arch, packager.computeSafeArtifactName(artifactName, "AppImage", arch, false))
118106
}
107+
108+
private async copyIcons(stageDir: string, resourceName: string): Promise<string> {
109+
const iconDirRelativePath = "usr/share/icons/hicolor"
110+
const iconDir = path.join(stageDir, iconDirRelativePath)
111+
await ensureDir(iconDir)
112+
113+
// https://github.com/AppImage/AppImageKit/issues/438#issuecomment-319094239
114+
// expects icons in the /usr/share/icons/hicolor
115+
const iconNames = await BluebirdPromise.map(this.helper.icons, async icon => {
116+
const filename = `${this.packager.executableName}.png`
117+
const iconSizeDir = `${icon.size}x${icon.size}/apps`
118+
const dir = path.join(iconDir, iconSizeDir)
119+
await ensureDir(dir)
120+
const finalIconFile = path.join(dir, filename)
121+
await copyOrLinkFile(icon.file, finalIconFile, null, true)
122+
123+
if (icon.file === this.helper.maxIconPath) {
124+
await symlink(path.relative(stageDir, finalIconFile), path.join(stageDir, filename))
125+
}
126+
return {filename, iconSizeDir, size: icon.size}
127+
})
128+
129+
let installIcons = ""
130+
for (const icon of iconNames) {
131+
installIcons += `xdg-icon-resource install --noupdate --context apps --size ${icon.size} "$APPDIR/${iconDirRelativePath}/${icon.iconSizeDir}/${icon.filename}" "${resourceName}"\n`
132+
}
133+
return installIcons
134+
}
119135
}
120136

121137
// https://unix.stackexchange.com/questions/202430/how-to-copy-a-directory-recursively-using-hardlinks-for-each-file
122-
function copyDirUsingHardLinks(source: string, destination: string, useLink: boolean) {
138+
function copyDirUsingHardLinks(source: string, destination: string) {
123139
if (process.platform !== "darwin") {
124140
const args = ["-d", "--recursive", "--preserve=mode"]
125-
if (useLink) {
126-
args.push("--link")
127-
}
141+
args.push("--link")
128142
args.push(source + "/", destination + "/")
129143
return ensureDir(path.dirname(destination)).then(() => exec("cp", args))
130144
}

packages/electron-publisher-s3/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
],
1313
"dependencies": {
1414
"fs-extra-p": "^4.4.2",
15-
"aws-sdk": "^2.126.0",
15+
"aws-sdk": "^2.127.0",
1616
"mime": "^2.0.3",
1717
"electron-publish": "~0.0.0-semantic-release",
1818
"builder-util": "^0.0.0-semantic-release",

test/src/helpers/fileAssert.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { exists } from "builder-util/out/fs"
1+
import { exists, statOrNull } from "builder-util/out/fs"
22
import { lstat, stat } from "fs-extra-p"
33
import * as path from "path"
44

@@ -22,7 +22,10 @@ class Assertions {
2222
}
2323

2424
async isFile() {
25-
const info = await stat(this.actual)
25+
const info = await statOrNull(this.actual)
26+
if (info == null) {
27+
throw new Error(`Path ${this.actual} doesn't exist`)
28+
}
2629
if (!info.isFile()) {
2730
throw new Error(`Path ${this.actual} is not a file`)
2831
}

test/src/helpers/packTester.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,13 @@ export async function assertPack(fixtureName: string, packagerOptions: PackagerO
9595
log(`Custom temp dir used: ${customTmpDir}`)
9696
}
9797

98-
await copyDir(projectDir, dir, it => {
99-
const basename = path.basename(it)
100-
return basename !== OUT_DIR_NAME && basename !== "node_modules" && !basename.startsWith(".")
101-
}, null, it => path.basename(it) !== "package.json")
98+
await copyDir(projectDir, dir, {
99+
filter: it => {
100+
const basename = path.basename(it)
101+
return basename !== OUT_DIR_NAME && basename !== "node_modules" && !basename.startsWith(".")
102+
},
103+
isUseHardLink: it => path.basename(it) !== "package.json",
104+
})
102105
projectDir = dir
103106

104107
await executeFinally((async () => {

test/src/linux/linuxPackagerTest.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ test.ifNotWindows.ifNotCiMac("AppImage - default icon, custom executable and cus
4545
projectDirCreated: it => remove(path.join(it, "build")),
4646
packed: async context => {
4747
const projectDir = context.getContent(Platform.LINUX)
48-
await assertThat(path.join(projectDir, "foo")).isFile()
48+
await assertThat(path.join(projectDir, "Foo")).isFile()
4949
},
5050
}))
5151

0 commit comments

Comments
 (0)