Skip to content

Commit

Permalink
fix(AppImage): restore appimaged compatibility (AppImage 2)
Browse files Browse the repository at this point in the history
  • Loading branch information
develar committed Oct 6, 2017
1 parent 65fcb71 commit b373275
Show file tree
Hide file tree
Showing 10 changed files with 76 additions and 40 deletions.
1 change: 1 addition & 0 deletions .idea/dictionaries/develar.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"///": "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",
"test": "node ./test/out/helpers/runTests.js skipArtifactPublisher ALL_TESTS=isCi",
"test-all": "yarn pretest && node ./test/out/helpers/runTests.js",
"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\"",
"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\"",
"whitespace": "whitespace 'src/**/*.ts'",
"docker-images": "docker/build.sh",
"update-deps": "npm-check-updates -a -x gitbook-plugin-github && node ./scripts/update-deps.js",
Expand All @@ -31,7 +31,7 @@
"7zip-bin": "^2.2.4",
"archiver": "^2.0.3",
"async-exit-hook": "^2.0.1",
"aws-sdk": "^2.126.0",
"aws-sdk": "^2.127.0",
"bluebird-lst": "^1.0.3",
"chalk": "^2.1.0",
"chromium-pickle-js": "^0.2.0",
Expand Down
23 changes: 18 additions & 5 deletions packages/builder-util/src/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,12 @@ export class FileCopier {
isUseHardLink: boolean

constructor(private readonly isUseHardLinkFunction?: (file: string) => boolean, private readonly transformer?: FileTransformer | null) {
this.isUseHardLink = _isUseHardLink && isUseHardLinkFunction !== DO_NOT_USE_HARD_LINKS
if (isUseHardLinkFunction === USE_HARD_LINKS) {
this.isUseHardLink = true
}
else {
this.isUseHardLink = _isUseHardLink && isUseHardLinkFunction !== DO_NOT_USE_HARD_LINKS
}
}

async copy(src: string, dest: string, stat: Stats | undefined) {
Expand All @@ -228,7 +233,8 @@ export class FileCopier {
}
}
}
await copyOrLinkFile(src, dest, stat, (!this.isUseHardLink || this.isUseHardLinkFunction == null) ? this.isUseHardLink : this.isUseHardLinkFunction(dest), this.isUseHardLink ? () => {
const isUseHardLink = (!this.isUseHardLink || this.isUseHardLinkFunction == null) ? this.isUseHardLink : this.isUseHardLinkFunction(dest)
await copyOrLinkFile(src, dest, stat, isUseHardLink, isUseHardLink ? () => {
// 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
if (this.isUseHardLink) {
this.isUseHardLink = false
Expand All @@ -241,20 +247,26 @@ export class FileCopier {
}
}

export interface CopyDirOptions {
filter?: Filter | null
transformer?: FileTransformer | null
isUseHardLink?: (file: string) => boolean
}

/**
* Empty directories is never created.
* Hard links is used if supported and allowed.
*/
export function copyDir(src: string, destination: string, filter?: Filter | null, transformer?: FileTransformer | null, isUseHardLink?: (file: string) => boolean): Promise<any> {
const fileCopier = new FileCopier(isUseHardLink, transformer)
export function copyDir(src: string, destination: string, options: CopyDirOptions = {}): Promise<any> {
const fileCopier = new FileCopier(options.isUseHardLink, options.transformer)

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

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

export const DO_NOT_USE_HARD_LINKS = (file: string) => false
export const USE_HARD_LINKS = (file: string) => true

export interface Link {
readonly link: string,
Expand Down
2 changes: 1 addition & 1 deletion packages/electron-builder/src/fileMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,6 @@ export function copyFiles(matchers: Array<FileMatcher> | null): Promise<any> {
if (debug.enabled) {
debug(`Copying files using pattern: ${matcher}`)
}
return await copyDir(matcher.from, matcher.to, matcher.createFilter())
return await copyDir(matcher.from, matcher.to, {filter: matcher.createFilter()})
})
}
4 changes: 3 additions & 1 deletion packages/electron-builder/src/packager/dirPackager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ async function unpack(packager: PlatformPackager<any>, out: string, platform: st
const destination = packager.getElectronDestinationDir(out)
log(`Copying Electron from "${source}" to "${destination}"`)
await emptyDir(out)
await copyDir(source, destination, null, null, DO_NOT_USE_HARD_LINKS)
await copyDir(source, destination, {
isUseHardLink: DO_NOT_USE_HARD_LINKS,
})
}

if (platform === "linux") {
Expand Down
60 changes: 37 additions & 23 deletions packages/electron-builder/src/targets/appImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import BluebirdPromise from "bluebird-lst"
import { Arch, exec, log, debug } from "builder-util"
import { UUID } from "builder-util-runtime"
import { getBinFromGithub } from "builder-util/out/binDownload"
import { unlinkIfExists, copyOrLinkFile } from "builder-util/out/fs"
import { unlinkIfExists, copyOrLinkFile, copyDir, USE_HARD_LINKS } from "builder-util/out/fs"
import * as ejs from "ejs"
import { emptyDir, ensureDir, readFile, remove, writeFile } from "fs-extra-p"
import { emptyDir, ensureDir, readFile, remove, symlink, writeFile } from "fs-extra-p"
import { Lazy } from "lazy-val"
import * as path from "path"
import { Target } from "../core"
Expand Down Expand Up @@ -45,24 +45,10 @@ export default class AppImageTarget extends Target {
const stageDir = path.join(this.outDir, `__appimage-${Arch[arch]}`)
const appInStageDir = path.join(stageDir, "app")
await emptyDir(stageDir)
await copyDirUsingHardLinks(appOutDir, appInStageDir, true)

const iconNames = await BluebirdPromise.map(this.helper.icons, it => {
let filename = `icon-${it.size}.png`
if (it.file === this.helper.maxIconPath) {
// largest icon as package icon
filename = `${this.packager.executableName}.png`
}
return copyOrLinkFile(it.file, path.join(stageDir, filename), null, true)
.then(() => ({filename, size: it.size}))
})
await copyDirUsingHardLinks(appOutDir, appInStageDir)

const resourceName = `appimagekit-${this.packager.executableName}`

let installIcons = ""
for (const icon of iconNames) {
installIcons += `xdg-icon-resource install --noupdate --context apps --size ${icon.size} "$APPDIR/${icon.filename}" "${resourceName}"\n`
}
const installIcons = await this.copyIcons(stageDir, resourceName)

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

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

if (this.packager.packagerOptions.effectiveOptionComputed != null && await this.packager.packagerOptions.effectiveOptionComputed({desktop: await this.desktopEntry.value})) {
Expand Down Expand Up @@ -116,15 +104,41 @@ export default class AppImageTarget extends Target {
}
packager.dispatchArtifactCreated(resultFile, this, arch, packager.computeSafeArtifactName(artifactName, "AppImage", arch, false))
}

private async copyIcons(stageDir: string, resourceName: string): Promise<string> {
const iconDirRelativePath = "usr/share/icons/hicolor"
const iconDir = path.join(stageDir, iconDirRelativePath)
await ensureDir(iconDir)

// https://github.com/AppImage/AppImageKit/issues/438#issuecomment-319094239
// expects icons in the /usr/share/icons/hicolor
const iconNames = await BluebirdPromise.map(this.helper.icons, async icon => {
const filename = `${this.packager.executableName}.png`
const iconSizeDir = `${icon.size}x${icon.size}/apps`
const dir = path.join(iconDir, iconSizeDir)
await ensureDir(dir)
const finalIconFile = path.join(dir, filename)
await copyOrLinkFile(icon.file, finalIconFile, null, true)

if (icon.file === this.helper.maxIconPath) {
await symlink(path.relative(stageDir, finalIconFile), path.join(stageDir, filename))
}
return {filename, iconSizeDir, size: icon.size}
})

let installIcons = ""
for (const icon of iconNames) {
installIcons += `xdg-icon-resource install --noupdate --context apps --size ${icon.size} "$APPDIR/${iconDirRelativePath}/${icon.iconSizeDir}/${icon.filename}" "${resourceName}"\n`
}
return installIcons
}
}

// https://unix.stackexchange.com/questions/202430/how-to-copy-a-directory-recursively-using-hardlinks-for-each-file
function copyDirUsingHardLinks(source: string, destination: string, useLink: boolean) {
function copyDirUsingHardLinks(source: string, destination: string) {
if (process.platform !== "darwin") {
const args = ["-d", "--recursive", "--preserve=mode"]
if (useLink) {
args.push("--link")
}
args.push("--link")
args.push(source + "/", destination + "/")
return ensureDir(path.dirname(destination)).then(() => exec("cp", args))
}
Expand Down
2 changes: 1 addition & 1 deletion packages/electron-publisher-s3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
],
"dependencies": {
"fs-extra-p": "^4.4.2",
"aws-sdk": "^2.126.0",
"aws-sdk": "^2.127.0",
"mime": "^2.0.3",
"electron-publish": "~0.0.0-semantic-release",
"builder-util": "^0.0.0-semantic-release",
Expand Down
7 changes: 5 additions & 2 deletions test/src/helpers/fileAssert.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { exists } from "builder-util/out/fs"
import { exists, statOrNull } from "builder-util/out/fs"
import { lstat, stat } from "fs-extra-p"
import * as path from "path"

Expand All @@ -22,7 +22,10 @@ class Assertions {
}

async isFile() {
const info = await stat(this.actual)
const info = await statOrNull(this.actual)
if (info == null) {
throw new Error(`Path ${this.actual} doesn't exist`)
}
if (!info.isFile()) {
throw new Error(`Path ${this.actual} is not a file`)
}
Expand Down
11 changes: 7 additions & 4 deletions test/src/helpers/packTester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,13 @@ export async function assertPack(fixtureName: string, packagerOptions: PackagerO
log(`Custom temp dir used: ${customTmpDir}`)
}

await copyDir(projectDir, dir, it => {
const basename = path.basename(it)
return basename !== OUT_DIR_NAME && basename !== "node_modules" && !basename.startsWith(".")
}, null, it => path.basename(it) !== "package.json")
await copyDir(projectDir, dir, {
filter: it => {
const basename = path.basename(it)
return basename !== OUT_DIR_NAME && basename !== "node_modules" && !basename.startsWith(".")
},
isUseHardLink: it => path.basename(it) !== "package.json",
})
projectDir = dir

await executeFinally((async () => {
Expand Down
2 changes: 1 addition & 1 deletion test/src/linux/linuxPackagerTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ test.ifNotWindows.ifNotCiMac("AppImage - default icon, custom executable and cus
projectDirCreated: it => remove(path.join(it, "build")),
packed: async context => {
const projectDir = context.getContent(Platform.LINUX)
await assertThat(path.join(projectDir, "foo")).isFile()
await assertThat(path.join(projectDir, "Foo")).isFile()
},
}))

Expand Down

0 comments on commit b373275

Please sign in to comment.