Skip to content

Commit

Permalink
feat: Build AppImage for Linux
Browse files Browse the repository at this point in the history
Closes #504
  • Loading branch information
develar committed Jul 5, 2016
1 parent de01c6d commit a9afdd4
Show file tree
Hide file tree
Showing 20 changed files with 333 additions and 66 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 CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Must be one of the following:
* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation.

### Scope
The scope is optional and could be anything specifying place of the commit change. For example `nsis`, `osx`, `linux`, etc...
The scope is optional and could be anything specifying place of the commit change. For example `nsis`, `mac`, `linux`, etc...

### Subject
The subject contains succinct description of the change:
Expand Down
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/Code Signing.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ To sign app on build server you need to set `CSC_LINK`, `CSC_KEY_PASSWORD` (and

1. [Export](https://developer.apple.com/library/ios/documentation/IDEs/Conceptual/AppDistributionGuide/MaintainingCertificates/MaintainingCertificates.html#//apple_ref/doc/uid/TP40012582-CH31-SW7) certificate.
Consider to not use special characters (for bash) in the password because “*values are not escaped when your builds are executed*”.
2. Encode file to base64 (osx/linux: `base64 -i yourFile.p12 -o envValue.txt`).
2. Encode file to base64 (macOS/linux: `base64 -i yourFile.p12 -o envValue.txt`).

Or upload `*.p12` file (e.g. on Google Drive, use [direct link generator](http://www.syncwithtech.org/p/direct-download-link-generator.html) to get correct download link).

Expand Down
19 changes: 10 additions & 9 deletions docs/Multi Platform Build.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,43 +10,44 @@ Don't think that mentioned issues are major, you should use build servers — e.
See [sample appveyor.yml](https://github.com/develar/onshape-desktop-shell/blob/master/appveyor.yml) to build Electron app for Windows.
And [sample .travis.yml](https://github.com/develar/onshape-desktop-shell/blob/master/.travis.yml) to build Electron app for MacOS.

By default build for current platform and current arch. Use CLI flags `--osx`, `--win`, `--linux` to specify platforms. And `--ia32`, `--x64` to specify arch.
By default build for current platform and current arch. Use CLI flags `--mac`, `--win`, `--linux` to specify platforms. And `--ia32`, `--x64` to specify arch.

For example, to build app for MacOS, Windows and Linux:
```
build -mwl
```

Build performed in parallel, so, it is highly recommended to not use npm task per platform (e.g. `npm run dist:osx && npm run dist:win32`), but specify multiple platforms/targets in one build command.
Build performed in parallel, so, it is highly recommended to not use npm task per platform (e.g. `npm run dist:mac && npm run dist:win32`), but specify multiple platforms/targets in one build command.
You don't need to clean dist output before build — output directory is cleaned automatically.

## MacOS

Use [brew](http://brew.sh) to install required packages.

To build app in distributable format for Windows on MacOS:
### To build app for Windows on MacOS:
```
brew install Caskroom/cask/xquartz wine mono
```

To build app in distributable format for Linux on MacOS:
### To build app for Linux on MacOS:
```
brew install gnu-tar libicns graphicsmagick
brew install gnu-tar libicns graphicsmagick xz
```

To build rpm: `brew install rpm`.

## Linux

To build app in distributable format for Linux:
```
sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils
sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils xorriso
```

To build rpm: `sudo apt-get install --no-install-recommends -y rpm`.

To build pacman: `sudo apt-get install --no-install-recommends -y bsdtar`.

To build app in distributable format for Windows on Linux:
### To build app for Windows on Linux:
* Install Wine (1.8+ is required):

```
Expand All @@ -69,7 +70,7 @@ To build app in distributable format for Windows on Linux:
apt-get install --no-install-recommends -y osslsigncode
```

To build app in 32 bit from a machine with 64 bit:
### To build app in 32 bit from a machine with 64 bit:

```
sudo apt-get install --no-install-recommends -y gcc-multilib g++-multilib
Expand All @@ -84,4 +85,4 @@ dist: trusty
## Windows
Not documented yet.
Use [Docker](https://github.com/electron-userland/electron-builder/wiki/Docker).
4 changes: 2 additions & 2 deletions docs/Options.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
In the development `package.json` custom `build` field can be specified to customize format:
```json
"build": {
"osx": {
"dmg": {
"contents": [
{
"x": 410,
Expand Down 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
2 changes: 1 addition & 1 deletion src/cliOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function createYargs(): any {
.example("build --win --ia32", "build for Windows ia32")
.option("mac", {
group: buildGroup,
alias: ["m", "o", "osx"],
alias: ["m", "o", "osx", "macos"],
describe: `Build for MacOS, accepts target list (see ${underline("https://goo.gl/HAnnq8")}).`,
type: "array",
})
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)
}
}
Loading

0 comments on commit a9afdd4

Please sign in to comment.