Skip to content

Commit

Permalink
feat: Build AppImage for Linux
Browse files Browse the repository at this point in the history
  • Loading branch information
develar committed Jul 5, 2016
1 parent de01c6d commit 6919b44
Show file tree
Hide file tree
Showing 16 changed files with 319 additions and 53 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.

2 changes: 1 addition & 1 deletion docker/4/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM electronuserland/electron-builder:base

ENV NODE_VERSION 4.4.6
ENV NODE_VERSION 4.4.7

# https://github.com/npm/npm/issues/4531
RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
Expand Down
6 changes: 6 additions & 0 deletions docker/appImage.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -e

dir=${PWD##*/}
rm -rf ../${dir}.7z
7za a -m0=lzma2 -mx=9 -mfb=64 -md=64m -ms=on ../${dir}.7z .
2 changes: 1 addition & 1 deletion docker/base/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ ENV FORCE_COLOR true
# libcurl4-openssl-dev, libtool and automake are required to build osslsigncode

RUN apt-get update -y && \
apt-get install --no-install-recommends -y bsdtar build-essential autoconf automake libcurl4-openssl-dev libtool libssl-dev icnsutils graphicsmagick gcc-multilib g++-multilib libgnome-keyring-dev lzip rpm yasm && \
apt-get install --no-install-recommends -y xorriso bsdtar build-essential autoconf automake libcurl4-openssl-dev libtool libssl-dev icnsutils graphicsmagick gcc-multilib g++-multilib libgnome-keyring-dev lzip rpm yasm && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
curl -L http://tukaani.org/xz/xz-$XZ_VERSION.tar.xz | tar -xJ && cd xz-$XZ_VERSION && ./configure && make && make install && cd .. && rm -rf xz-$XZ_VERSION && ldconfig && \
Expand Down
2 changes: 1 addition & 1 deletion docs/Options.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Here documented only `electron-builder` specific options:
| --- | ---
| **name** | <a name="AppMetadata-name"></a>The application name.
| productName | <a name="AppMetadata-productName"></a><p>As [name](#AppMetadata-name), but allows you to specify a product name for your executable which contains spaces and other special characters not allowed in the [name property](https://docs.npmjs.com/files/package.json#name}).</p>
| **description** | <a name="AppMetadata-description"></a>The application description.
| description | <a name="AppMetadata-description"></a>The application description.
| homepage | <a name="AppMetadata-homepage"></a><p>The url to the project [homepage](https://docs.npmjs.com/files/package.json#homepage) (NuGet Package <code>projectUrl</code> (optional) or Linux Package URL (required)).</p> <p>If not specified and your project repository is public on GitHub, it will be <code>https://github.com/${user}/${project}</code> by default.</p>
| license | <a name="AppMetadata-license"></a>*linux-only.* The [license](https://docs.npmjs.com/files/package.json#license) name.

Expand Down
8 changes: 4 additions & 4 deletions src/appInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import sanitizeFileName = require("sanitize-filename")
const __awaiter = require("./util/awaiter")

export class AppInfo {
readonly description = smarten(this.metadata.description)
readonly description = smarten(this.metadata.description!)

// windows-only
versionString = {
Expand All @@ -27,7 +27,7 @@ export class AppInfo {
readonly productFilename: string

constructor(public metadata: AppMetadata, private devMetadata: DevMetadata) {
let buildVersion = metadata.version
let buildVersion = metadata.version!
this.version = buildVersion

const buildNumber = this.buildNumber
Expand All @@ -40,7 +40,7 @@ export class AppInfo {
}

get companyName() {
return this.metadata.author.name
return this.metadata.author!.name
}

get buildNumber(): string | null {
Expand Down Expand Up @@ -76,7 +76,7 @@ export class AppInfo {
if (copyright != null) {
return copyright
}
return `Copyright © ${new Date().getFullYear()} ${this.metadata.author.name || this.productName}`
return `Copyright © ${new Date().getFullYear()} ${this.metadata.author!.name || this.productName}`
}

async computePackageUrl(): Promise<string | null> {
Expand Down
5 changes: 3 additions & 2 deletions src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ export async function createPublisher(packager: Packager, options: PublishOption
throw new Error(`Please specify 'repository' in the dev package.json ('${packager.devPackageFile}')`)
}
else {
log(`Creating Github Publisher — user: ${info.user}, project: ${info.project}, version: ${packager.metadata.version}`)
return new GitHubPublisher(info.user, info.project, packager.metadata.version, options, isPublishOptionGuessed)
const version = packager.metadata.version!
log(`Creating Github Publisher — user: ${info.user}, project: ${info.project}, version: ${version}`)
return new GitHubPublisher(info.user, info.project, version, options, isPublishOptionGuessed)
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface Metadata {
# Application `package.json`
*/
export interface AppMetadata extends Metadata {
readonly version: string
readonly version?: string

/*
The application name.
Expand All @@ -25,11 +25,11 @@ export interface AppMetadata extends Metadata {
/*
The application description.
*/
readonly description: string
readonly description?: string

readonly main?: string | null

readonly author: AuthorMetadata
readonly author?: AuthorMetadata

/*
The url to the project [homepage](https://docs.npmjs.com/files/package.json#homepage) (NuGet Package `projectUrl` (optional) or Linux Package URL (required)).
Expand Down
16 changes: 12 additions & 4 deletions src/packager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,15 @@ export class Packager implements BuildInfo {
this.isTwoPackageJsonProjectLayoutUsed = this.appDir !== this.projectDir

const appPackageFile = this.projectDir === this.appDir ? devPackageFile : path.join(this.appDir, "package.json")
this.metadata = appPackageFile === devPackageFile ? (this.options.appMetadata || this.devMetadata) : deepAssign(await readPackageJson(appPackageFile), this.options.appMetadata)
if (appPackageFile === devPackageFile) {
if (this.options.appMetadata != null) {
this.devMetadata = deepAssign(this.devMetadata, this.options.appMetadata)
}
this.metadata = <any>this.devMetadata
}
else {
this.metadata = deepAssign(await readPackageJson(appPackageFile), this.options.appMetadata)
}

this.checkMetadata(appPackageFile, devPackageFile)
checkConflictingOptions(this.devMetadata.build)
Expand Down Expand Up @@ -147,7 +155,7 @@ export class Packager implements BuildInfo {
throw new Error(`Please specify '${missedFieldName}' in the application package.json ('${appPackageFile}')`)
}

const checkNotEmpty = (name: string, value: string) => {
const checkNotEmpty = (name: string, value: string | n) => {
if (isEmptyOrSpaces(value)) {
reportError(name)
}
Expand Down Expand Up @@ -178,10 +186,10 @@ export class Packager implements BuildInfo {
}
else {
const author = appMetadata.author
if (<any>author == null) {
if (author == null) {
throw new Error(`Please specify "author" in the application package.json ('${appPackageFile}') — it is used as company name.`)
}
else if (<any>author.email == null && this.options.targets!.has(Platform.LINUX)) {
else if (author.email == null && this.options.targets!.has(Platform.LINUX)) {
throw new Error(util.format(errorMessages.authorEmailIsMissed, appPackageFile))
}

Expand Down
6 changes: 2 additions & 4 deletions src/targets/LinuxTargetHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export class LinuxTargetHelper {
}
}

async computeDesktopEntry(relativeExec: boolean): Promise<string> {
async computeDesktopEntry(exec?: string): Promise<string> {
const appInfo = this.packager.appInfo

const custom = this.packager.platformSpecificBuildOptions.desktop
Expand All @@ -73,13 +73,11 @@ export class LinuxTargetHelper {
}

const productFilename = appInfo.productFilename
const appExec = relativeExec ? `"${productFilename}"` : `"${installPrefix}/${productFilename}/${productFilename}"`

const tempFile = path.join(await this.tempDirPromise, `${productFilename}.desktop`)
await outputFile(tempFile, this.packager.platformSpecificBuildOptions.desktop || `[Desktop Entry]
Name=${appInfo.productName}
Comment=${this.packager.platformSpecificBuildOptions.description || appInfo.description}
Exec=${appExec}
Exec=${(exec == null ? `"${installPrefix}/${productFilename}/${productFilename}"` : exec)}
Terminal=false
Type=Application
Icon=${appInfo.name}
Expand Down
50 changes: 25 additions & 25 deletions src/targets/appImage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PlatformPackager, TargetEx } from "../platformPackager"
import { LinuxBuildOptions, Arch } from "../metadata"
import * as path from "path"
import { exec, unlinkIfExists } from "../util/util"
import { exec, unlinkIfExists, spawn, debug } from "../util/util"
import { open, write, createReadStream, createWriteStream, close, chmod } from "fs-extra-p"
import { LinuxTargetHelper } from "./LinuxTargetHelper"
import { getBin } from "../util/binDownload"
Expand All @@ -11,58 +11,51 @@ import { Promise as BluebirdPromise } from "bluebird"
const __awaiter = require("../util/awaiter")

const appImageVersion = "AppImage-5"
const appImagePathPromise = getBin("AppImage", appImageVersion, `https://dl.bintray.com/electron-userland/bin/${appImageVersion}.7z`, "")
const appImagePathPromise = getBin("AppImage", appImageVersion, `https://dl.bintray.com/electron-userland/bin/${appImageVersion}.7z`, "19833e5db3cbc546432de8ddc8a54181489e6faad4944bd1f3138adf4b771259")

export default class AppImageTarget extends TargetEx {
private readonly desktopEntry: Promise<string>

constructor(private packager: PlatformPackager<LinuxBuildOptions>, private helper: LinuxTargetHelper, private outDir: string) {
super("appImage")

this.desktopEntry = helper.computeDesktopEntry(true)
}

async build(appOutDir: string, arch: Arch): Promise<any> {
const packager = this.packager

const destination = path.join(this.outDir, packager.generateName(null, arch, true))
const image = path.join(this.outDir, packager.generateName(null, arch, true))
const appInfo = packager.appInfo

await unlinkIfExists(destination)
await unlinkIfExists(image)

const appImagePath = await appImagePathPromise
const args = [
"-joliet", "on",
"-volid", "AppImage",
"-dev", destination,
"-dev", image,
"-padding", "0",
"-map", appOutDir, "/usr/bin", "--",
"-map", await this.desktopEntry, `/${appInfo.name}.desktop`, "--",
"-map", path.join(appImagePath, arch === Arch.ia32 ? "32": "64", "AppRun"), "/AppRun", "--",
"-map", appOutDir, "/usr/bin",
"-map", path.join(__dirname, "..", "..", "templates", "linux", "AppRun.sh"), `/AppRun`,
"-move", `/usr/bin/${appInfo.productFilename}`, "/usr/bin/app",
]

for (let [from, to] of (await this.helper.icons)) {
args.push("-map", from, `/usr/share/icons/default/${to}`, "--",)
args.push("-map", from, `/usr/share/icons/default/${to}`)
}

// must be after this.helper.icons call
if (this.helper.maxIconPath == null) {
throw new Error("Icon is not provided")
}
args.push("-map", this.helper.maxIconPath, "/.DirIcon", "--",)
args.push("-map", this.helper.maxIconPath, `/${appInfo.name}${path.extname(this.helper.maxIconPath)}`, "--",)
args.push("-map", this.helper.maxIconPath, "/.DirIcon")

args.push("-zisofs", `level=${packager.devMetadata.build.compression === "store" ? "0" : "9"}:block_size=128k:by_magic=off`, "-chown_r", "0")
args.push("/", "--", "set_filter_r", "--zisofs", "/")
// args.push("-zisofs", `level=0:block_size=128k:by_magic=off`, "-chown_r", "0")
// args.push("/", "--", "set_filter_r", "--zisofs", "/")

await exec("xorriso", args)
await exec(process.platform === "darwin" ? path.join(appImagePath, "xorriso") : "xorriso", args)

const fd = await open(destination, "r+")
const fd = await open(image, "r+")
try {
await new BluebirdPromise((resolve, reject) => {
const rd = createReadStream(path.join(appImagePath, arch === Arch.ia32 ? "32" : "64", "AppRun"))
const rd = createReadStream(path.join(appImagePath, arch === Arch.ia32 ? "32" : "64", "runtime"))
rd.on("error", reject)
const wr = createWriteStream(destination, <any>{fd: fd, autoClose: false})
const wr = createWriteStream(image, <any>{fd: fd, autoClose: false})
wr.on("error", reject)
wr.on("finish", resolve)
rd.pipe(wr)
Expand All @@ -75,8 +68,15 @@ export default class AppImageTarget extends TargetEx {
await close(fd)
}

await chmod(destination, "0755")
await chmod(image, "0755")
// we archive because you cannot distribute exe as is - e.g. Ubuntu clear exec flag and user cannot just click on AppImage to run
// also, LZMA compression - 29MB vs zip 42MB
// we use slow xz instead of 7za because 7za doesn't preserve exec file permissions for xz
await spawn("xz", ["--compress", "--force", image], {
cwd: path.dirname(image),
stdio: ["ignore", debug.enabled ? "inherit" : "ignore", "inherit"],
})

packager.dispatchArtifactCreated(destination)
packager.dispatchArtifactCreated(image)
}
}
5 changes: 2 additions & 3 deletions src/targets/fpm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { smarten, PlatformPackager, TargetEx } from "../platformPackager"
import { use, exec } from "../util/util"
import * as path from "path"
import { downloadFpm } from "../util/binDownload"
import { tmpdir } from "os"
import { readFile, outputFile } from "fs-extra-p"
import { Promise as BluebirdPromise } from "bluebird"
import { LinuxTargetHelper, installPrefix } from "./LinuxTargetHelper"
Expand All @@ -27,7 +26,7 @@ export default class FpmTarget extends TargetEx {
super(name)

this.scriptFiles = this.createScripts(helper.tempDirPromise)
this.desktopEntry = helper.computeDesktopEntry(false)
this.desktopEntry = helper.computeDesktopEntry()
}

private async createScripts(tempDirPromise: Promise<string>): Promise<Array<string>> {
Expand Down Expand Up @@ -62,7 +61,7 @@ export default class FpmTarget extends TargetEx {
}

const options = this.options
const author = options.maintainer || `${packager.appInfo.metadata.author.name} <${packager.appInfo.metadata.author.email}>`
const author = options.maintainer || `${appInfo.metadata.author!.name} <${appInfo.metadata.author!.email}>`
const synopsis = options.synopsis
const args = [
"-s", "dir",
Expand Down
8 changes: 7 additions & 1 deletion src/util/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { readJson, stat, Stats, unlink } from "fs-extra-p"
import { yellow, red } from "chalk"
import debugFactory = require("debug")
import IDebugger = debug.IDebugger
import { warn, task } from "./log"
import { warn, task, log } from "./log"
import { createHash } from "crypto"

//noinspection JSUnusedLocalSymbols
Expand Down Expand Up @@ -79,6 +79,12 @@ export function exec(file: string, args?: Array<string> | null, options?: ExecOp
return new BluebirdPromise<string>((resolve, reject) => {
execFile(file, <any>args, options, function (error, stdout, stderr) {
if (error == null) {
if (debug.enabled) {
if (stderr.length !== 0) {
log(stderr)
}
// log(stdout)
}
resolve(stdout)
}
else {
Expand Down
Loading

0 comments on commit 6919b44

Please sign in to comment.