Skip to content

Commit

Permalink
feat(snap): build snap on macOS
Browse files Browse the repository at this point in the history
  • Loading branch information
develar committed Feb 9, 2018
1 parent 2a2d832 commit 138b229
Show file tree
Hide file tree
Showing 13 changed files with 96 additions and 156 deletions.
2 changes: 1 addition & 1 deletion docker/9/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM electronuserland/builder:base

ENV NODE_VERSION 9.4.0
ENV NODE_VERSION 9.5.0

# this package is used for snapcraft and we should not clear apt list - to avoid apt-get update during snap build
RUN apt-get -qq update && \
Expand Down
29 changes: 24 additions & 5 deletions docker/base/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,27 @@ RUN curl -L https://yarnpkg.com/latest.tar.gz | tar xvz && mv yarn-* /yarn && ln
# python for node-gyp
# rpm is required for FPM to build rpm package
# libpng16-16 is required for libicns1_0.8.1-3.1 (on xenial)
# TODO remove graphicsmagick (3 months after electron-builder 19.53.0)
# libsecret-1-0 is required even for prebuild keytar
# libsecret-1-0 and libgnome-keyring-dev are required even for prebuild keytar
# libgtk2.0-dev for snap desktop-gtk2 (see https://github.com/ubuntu/snapcraft-desktop-helpers/blob/master/snapcraft.yaml#L248)
apt-get install --no-install-recommends -y libsecret-1-0 git snapcraft qtbase5-dev bsdtar build-essential autoconf libssl-dev icnsutils libopenjp2-7 graphicsmagick gcc-multilib g++-multilib libgnome-keyring-dev lzip rpm python libcurl3 git git-lfs ssh libpng16-16 unzip libgtk2.0-dev && \
apt-get -qq install --no-install-recommends git qtbase5-dev bsdtar build-essential autoconf libssl-dev gcc-multilib g++-multilib lzip rpm python libcurl3 git git-lfs ssh unzip \
libpng16-16 icnsutils libopenjp2-7 \
libsecret-1-0 libgnome-keyring-dev \
libgtk2.0-dev && \
# libicns
curl -O http://mirrors.kernel.org/ubuntu/pool/universe/libi/libicns/libicns1_0.8.1-3.1_amd64.deb && dpkg --install libicns1_0.8.1-3.1_amd64.deb && unlink libicns1_0.8.1-3.1_amd64.deb && \
# git-lfs
git lfs install && \
# clean
# snap
apt-get -qq install --no-install-recommends jq squashfs-tools && \
curl -L $(curl -H 'X-Ubuntu-Series: 16' 'https://api.snapcraft.io/api/v1/snaps/details/core' | jq '.download_url' -r) --output core.snap && \
mkdir -p /snap/core && unsquashfs -d /snap/core/current core.snap && rm core.snap && \
curl -L $(curl -H 'X-Ubuntu-Series: 16' 'https://api.snapcraft.io/api/v1/snaps/details/snapcraft?channel=edge' | jq '.download_url' -r) --output snapcraft.snap && \
mkdir -p /snap/snapcraft && unsquashfs -d /snap/snapcraft/current snapcraft.snap && rm snapcraft.snap && \
mkdir -p /snap/bin && \
echo "#!/bin/sh" > /snap/bin/snapcraft && \
echo 'exec $SNAP/usr/bin/python3 $SNAP/bin/snapcraft "$@"' >> /snap/bin/snapcraft && \
chmod a+x /snap/bin/snapcraft && \
apt-get -qq purge jq squashfs-tools && \
rm -rf /var/lib/apt/lists/*

COPY test.sh /test.sh
Expand All @@ -34,4 +47,10 @@ ENV LC_ALL C.UTF-8
ENV USE_UNZIP true

ENV DEBUG_COLORS true
ENV FORCE_COLOR true
ENV FORCE_COLOR true

ENV SNAP=/snap/snapcraft/current
ENV SNAP_ARCH=amd64
ENV SNAP_NAME=snapcraft
ENV SNAP_VERSION=edge
ENV PATH=/snap/bin:$PATH
4 changes: 2 additions & 2 deletions docker/build.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env bash
set -e

docker build -t electronuserland/builder:base docker/base
docker build -t electronuserland/builder:base -t electronuserland/builder:base-03.18 docker/base

docker build -t electronuserland/builder:9 -t electronuserland/builder:latest docker/9
docker build -t electronuserland/builder:9 -t electronuserland/builder:latest -t electronuserland/builder:9-03.18 docker/9

docker build -t electronuserland/builder:wine docker/wine
docker build -t electronuserland/builder:wine-mono docker/wine-mono
Expand Down
2 changes: 1 addition & 1 deletion docker/wine/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ FROM electronuserland/builder:latest

RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common && dpkg --add-architecture i386 && curl -L https://dl.winehq.org/wine-builds/Release.key > Release.key && apt-key add Release.key && apt-add-repository https://dl.winehq.org/wine-builds/ubuntu && \
apt-get update && \
apt-get -y remove software-properties-common libdbus-glib-1-2 python3-dbus python3-gi python3-pycurl python3-software-properties && \
apt-get -y purge software-properties-common libdbus-glib-1-2 python3-dbus python3-gi python3-pycurl python3-software-properties && \
apt-get install -y --no-install-recommends winehq-stable && \
# clean
apt-get clean && rm -rf /var/lib/apt/lists/* && unlink Release.key
Expand Down
2 changes: 1 addition & 1 deletion packages/builder-util/src/binDownload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,6 @@ function doGetBin(name: string, url: string, checksum: string): Promise<string>
...process.env,
SZA_PATH: path7za,
},
stdio: ["ignore", "pipe", "inherit"]
stdio: ["ignore", "pipe", process.stdout]
})
}
157 changes: 38 additions & 119 deletions packages/electron-builder-lib/src/targets/snap.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isEnvTrue, Arch, exec, replaceDefault as _replaceDefault, serializeToYaml, spawn, toLinuxArchString } from "builder-util"
import { copyFile, copyDir, copyDirUsingHardLinks, USE_HARD_LINKS } from "builder-util/out/fs"
import { path7za } from "7zip-bin"
import { appBuilderPath } from "app-builder-bin"
import { isEnvTrue, Arch, replaceDefault as _replaceDefault, serializeToYaml, spawn, toLinuxArchString, log } from "builder-util"
import { outputFile } from "fs-extra-p"
import * as path from "path"
import { SnapOptions } from ".."
Expand All @@ -8,23 +9,8 @@ import { Target } from "../core"
import { LinuxPackager, toAppImageOrSnapArch } from "../linuxPackager"
import { PlugDescriptor } from "../options/SnapOptions"
import { LinuxTargetHelper } from "./LinuxTargetHelper"
import { createStageDir, StageDir } from "./targetUtil"
import BluebirdPromise from "bluebird-lst"
import { getSnapTemplate } from "./tools"

// usr/share/fonts is required, cannot run otherwise
const unnecessaryFiles = [
"usr/share/doc",
"usr/share/man",
"usr/share/icons",
"usr/share/bash-completion",
"usr/share/lintian",
"usr/share/dh-python",
"usr/share/python3",

"usr/lib/python*",
"usr/bin/python*",
]
import { createStageDir } from "./targetUtil"
import { SNAP_TEMPLATE_SHA512, SNAP_TEMPLATE_VERSION } from "./tools"

// libxss1, libasound2, gconf2 - was "error while loading shared libraries: libXss.so.1" on Xubuntu 16.04
const defaultStagePackages = ["libasound2", "libgconf2-4", "libnotify4", "libnspr4", "libnss3", "libpcre3", "libpulse0", "libxss1", "libxtst6"]
Expand All @@ -47,7 +33,7 @@ export default class SnapTarget extends Target {
return result
}

private createDescriptor(snapName: string, appOutDir: string, arch: Arch, isUseDocker: boolean): any {
private createDescriptor(snapName: string, arch: Arch): any {
const appInfo = this.packager.appInfo
const options = this.options
const linuxArchName = toAppImageOrSnapArch(arch)
Expand All @@ -64,7 +50,7 @@ export default class SnapTarget extends Target {
grade: options.grade || "stable",
apps: {
[snapName]: {
command: `bin/desktop-launch $SNAP/${this.packager.executableName}`,
command: `bin/desktop-launch $SNAP/app/${this.packager.executableName}`,
adapter: "none",
environment: {
TMPDIR: "$XDG_RUNTIME_DIR",
Expand All @@ -84,9 +70,8 @@ export default class SnapTarget extends Target {
},
parts: {
app: {
plugin: "dump",
plugin: "nil",
"stage-packages": this.replaceDefault(options.stagePackages, defaultStagePackages),
source: isUseDocker ? "/appOutDir" : appOutDir,
after: this.replaceDefault(options.after, ["desktop-gtk2"]),
}
},
Expand Down Expand Up @@ -117,36 +102,36 @@ export default class SnapTarget extends Target {
const options = this.options
const snapName = packager.executableName.toLowerCase()
const buildPackages = asArray(options.buildPackages)
const isUseDocker = process.platform !== "linux" || isEnvTrue(process.env.SNAP_USE_DOCKER)
this.isUseTemplateApp = this.options.useTemplateApp !== false && arch === Arch.x64 && buildPackages.length === 0

const snapFileName = `${snapName}_${packager.appInfo.version}_${toLinuxArchString(arch)}.snap`
const artifactPath = path.join(this.outDir, snapFileName)
this.logBuilding("snap", artifactPath, arch)

const snap: any = this.createDescriptor(snapName, appOutDir, arch, isUseDocker)
const snap: any = this.createDescriptor(snapName, arch)
if (this.isUseTemplateApp) {
delete snap.parts
}

const stageDir = await createStageDir(this, packager, arch)
// snapcraft.yaml inside a snap directory
const snapDir = path.join(stageDir.dir, "snap")
const snapMetaDir = this.isUseTemplateApp ? path.join(stageDir.dir, "meta") : snapDir
const snapMetaDir = path.join(stageDir.dir, this.isUseTemplateApp ? "meta" : "snap")

const args = [
"snap",
"--app", appOutDir,
"--stage", stageDir.dir,
"--arch", toLinuxArchString(arch),
"--output", artifactPath,
"--docker-image", "electronuserland/builder:latest"
]

await this.helper.icons
if (this.helper.maxIconPath != null) {
if (!this.isUseTemplateApp) {
snap.icon = "snap/gui/icon.png"
}
await copyFile(this.helper.maxIconPath, path.join(snapMetaDir, "gui", "icon.png"))
}

const hooksDir = await packager.getResource(options.hooks, "snap-hooks")
if (hooksDir != null) {
await copyDir(hooksDir, path.join(snapMetaDir, "hooks"), {
isUseHardLink: USE_HARD_LINKS,
})
args.push("--icon", this.helper.maxIconPath)
}

const desktopFile = path.join(snapMetaDir, "gui", `${snap.name}.desktop`)
Expand All @@ -159,99 +144,33 @@ export default class SnapTarget extends Target {
return
}

const snapcraftFile = path.join(snapMetaDir, this.isUseTemplateApp ? "snap.yaml" : "snapcraft.yaml")
await outputFile(snapcraftFile, serializeToYaml(snap))
if (this.isUseTemplateApp) {
await copyDir(await getSnapTemplate(), stageDir.dir, {
isUseHardLink: USE_HARD_LINKS,
})
await copyDirUsingHardLinks(appOutDir, stageDir.dir)
}
await outputFile(path.join(snapMetaDir, this.isUseTemplateApp ? "snap.yaml" : "snapcraft.yaml"), serializeToYaml(snap))

if (isUseDocker) {
if (this.isUseTemplateApp) {
await this.buildUsingDockerAndPrepackedSnap(snapFileName, stageDir)
}
else {
await this.buildUsingDocker(options, arch, snapFileName, stageDir, appOutDir)
}
}
else {
await this.buildWithoutDocker(buildPackages, stageDir.dir, arch, artifactPath)
if (log.isDebugEnabled && !isEnvTrue(process.env.ELECTRON_BUILDER_REMOVE_STAGE_EVEN_IF_DEBUG)) {
args.push("--no-remove-stage")
}

await stageDir.cleanup()
packager.dispatchArtifactCreated(artifactPath, this, arch)
}

private async buildWithoutDocker(buildPackages: Array<string>, stageDir: string, arch: Arch, artifactPath: string) {
if (buildPackages.length > 0) {
const notInstalledPackages = await BluebirdPromise.filter(buildPackages, (it): Promise<boolean> => {
return exec("dpkg", ["-s", it])
.then(result => result.includes("is not installed"))
})
if (notInstalledPackages.length > 0) {
await spawn("apt-get", ["-qq", "update"])
await spawn("apt-get", ["-qq", "install", "--no-install-recommends"].concat(notInstalledPackages))
}
}
const spawnOptions = {
cwd: stageDir,
stdio: ["ignore", "inherit", "inherit"],
const hooksDir = await packager.getResource(options.hooks, "snap-hooks")
if (hooksDir != null) {
args.push("--hooks", hooksDir)
}

let primeDir: string
if (this.isUseTemplateApp) {
primeDir = stageDir
const templateDirName = `snap-template-${SNAP_TEMPLATE_VERSION}`
args.push(
"--template-url", `https://github.com/electron-userland/electron-builder-binaries/releases/download/${templateDirName}/${templateDirName}.7z`,
"--template-sha512", SNAP_TEMPLATE_SHA512,
)
}
else {
await spawn("snapcraft", ["prime", "--target-arch", toLinuxArchString(arch)], spawnOptions)
primeDir = stageDir + path.sep + "prime"
await exec("/bin/bash", ["-c", `rm -rf ${unnecessaryFiles.join(" ")}`], {
cwd: primeDir,
})
}
await spawn("snapcraft", ["pack", primeDir, "--output", artifactPath], spawnOptions)
}

private async buildUsingDockerAndPrepackedSnap(snapFileName: string, stageDir: StageDir) {
await spawn("docker", ["run", "--rm",
// dist dir can be outside of project dir
"-v", `${this.outDir}:/out`,
"-v", `${stageDir.dir}:/stage:ro`,
"electronuserland/builder:latest",
"/bin/bash", "-c", `snapcraft pack /stage --output /out/${snapFileName}`,
], {
cwd: this.packager.info.projectDir,
stdio: ["ignore", "inherit", "inherit"],
})
}

private async buildUsingDocker(options: SnapOptions, arch: Arch, snapFileName: string, stageDir: StageDir, appOutDir: string) {
const commands: Array<string> = []
if (options.buildPackages != null && options.buildPackages.length > 0) {
commands.push(`apt-get install --no-install-recommends -y ${options.buildPackages.join(" ")}`)
}

// copy stage to linux fs to avoid performance issues (https://docs.docker.com/docker-for-mac/osxfs-caching/)
commands.push("cp -R /stage /s/")
commands.push("cd /s")
commands.push(`snapcraft prime --target-arch ${toLinuxArchString(arch)}`)
commands.push(`rm -rf ${unnecessaryFiles.map(it => `prime/${it}`).join(" ")}`)
commands.push(`snapcraft pack /s/prime --output /out/${snapFileName}`)

await spawn("docker", ["run", "--rm",
"-v", `${this.packager.info.projectDir}:/project:delegated`,
// dist dir can be outside of project dir
"-v", `${this.outDir}:/out`,
"-v", `${stageDir.dir}:/stage:ro`,
"-v", `${appOutDir}:/appOutDir:ro`,
"electronuserland/builder:latest",
"/bin/bash", "-c", commands.join(" && "),
], {
cwd: this.packager.info.projectDir,
stdio: ["ignore", "inherit", "inherit"],
await spawn(appBuilderPath, args, {
env: {
...process.env,
SZA_PATH: path7za,
},
stdio: ["ignore", "inherit", "inherit"]
})
packager.dispatchArtifactCreated(artifactPath, this, arch)
}
}

Expand Down
15 changes: 9 additions & 6 deletions packages/electron-builder-lib/src/targets/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { Lazy } from "lazy-val"
import * as path from "path"
import { Platform } from "../core"

export const SNAP_TEMPLATE_VERSION = "0.1.1"
// noinspection SpellCheckingInspection
export const SNAP_TEMPLATE_SHA512 = "W8JXQMwsrqH7T8kFD3KuULNVJRqygmcQPDPGhr9BXeRQS9U+A6jSsUEopQIwfQxlhuA6f7Jerc9XA0/ZLlK60w=="

export function getLinuxToolsPath() {
//noinspection SpellCheckingInspection
return getBinFromGithub("linux-tools", "mac-10.12.3", "SQ8fqIRVXuQVWnVgaMTDWyf2TLAJjJYw3tRSqQJECmgF6qdM7Kogfa6KD49RbGzzMYIFca9Uw3MdsxzOPRWcYw==")
Expand Down Expand Up @@ -35,7 +39,11 @@ export const fpmPath = new Lazy(() => {
// noinspection JSUnusedGlobalSymbols
export function prefetchBuildTools(): Promise<any> {
// yes, we starting to use native Promise
return Promise.all([getAppImage(), fpmPath.value, getSnapTemplate()])
return Promise.all([
getAppImage(),
fpmPath.value,
getBinFromGithub("snap-template", SNAP_TEMPLATE_VERSION, SNAP_TEMPLATE_SHA512),
])
}

export function getZstd() {
Expand Down Expand Up @@ -71,11 +79,6 @@ export function getAria() {
.then(it => path.join(it, `aria2c${platform === Platform.WINDOWS ? ".exe" : ""}`))
}

export function getSnapTemplate() {
// noinspection SpellCheckingInspection
return getBinFromGithub("snap-template", "0.1.1", "W8JXQMwsrqH7T8kFD3KuULNVJRqygmcQPDPGhr9BXeRQS9U+A6jSsUEopQIwfQxlhuA6f7Jerc9XA0/ZLlK60w==")
}

export interface ToolDescriptor {
name: string
version: string
Expand Down
12 changes: 8 additions & 4 deletions packages/electron-updater/src/AppImageUpdater.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import BluebirdPromise from "bluebird-lst"
import { AllPublishOptions, CancellationToken, DownloadOptions, newError, UpdateInfo } from "builder-util-runtime"
import { execFileSync, spawn } from "child_process"
import isDev from "electron-is-dev"
Expand All @@ -17,12 +16,17 @@ export class AppImageUpdater extends BaseUpdater {

checkForUpdatesAndNotify(): Promise<UpdateCheckResult | null> {
if (isDev) {
return BluebirdPromise.resolve(null)
return Promise.resolve(null)
}

if (process.env.APPIMAGE == null) {
this._logger.warn("APPIMAGE env is not defined, current application is not an AppImage")
return BluebirdPromise.resolve(null)
if (process.env.SNAP == null) {
this._logger.warn("APPIMAGE env is not defined, current application is not an AppImage")
}
else {
this._logger.info("SNAP env is defined, updater is disabled")
}
return Promise.resolve(null)
}

return super.checkForUpdatesAndNotify()
Expand Down
Loading

0 comments on commit 138b229

Please sign in to comment.