Compare commits

...

59 Commits

Author SHA1 Message Date
94625fce38 chore(release): 0.6.11 2021-07-17 13:08:10 +01:00
b03343f506 fix(native-comp): fix re-linking and signing issue with *.eln files
With the recent move of shared libraries and native lisp *.eln files to
Contents/Frameworks, the re-linking paths became longer, causing code
signing to fail with headerpad errors. This change ensures there's
enough space within binary files for longer shared library relink paths,
and code signing payloads.
2021-07-17 13:04:33 +01:00
b1896d4a4f chore(release): 0.6.10 2021-07-17 00:20:42 +01:00
9d32509c61 fix(native-comp): *.eln files were not being found during shared lib embedding 2021-07-16 21:00:24 +01:00
1c2745cd36 docs(readme): mention binary builds repo 2021-07-10 21:19:11 +01:00
80a0d55b24 chore(release): 0.6.9 2021-07-04 23:28:28 +01:00
fd0ec4d772 fix(plan): correctly parse --test-release-type flag
The --test-release-type flag was essentially broken and ignored, always
creating a prerelease release for test builds. Now it can also produce
draft releases.
2021-07-04 23:27:00 +01:00
c0c809a86a style(lint): fix linting complaint 2021-07-03 02:39:01 +01:00
cb63806262 feat(release): add bulk edit command to quickly change multiple GitHub releases 2021-07-03 02:36:15 +01:00
6d7ab95ca2 chore(release): 0.6.8 2021-07-03 00:19:26 +01:00
f4d6e3a56d feat(builds): add support for stable builds
Stable builds are based off of release git tags in Emacs' git
repo. Examples of what release tags look like:

- emacs-26.1
- emacs-26.2
- emacs-26.3
- emacs-27.1
- emacs-27.2

When the specified git ref looks like a stable release, the plan command
will generate a release a different and simpler release name that does
not include the date, git sha or ref. Instead, for "emacs-27.2" for
example, the emacs-builds release name will be "Emacs-27.2".

The "build name", used for naming the disk image, still retains the same
format as the nightly builds.

Also, non-stable releases are now marked as pre-release on GitHub by
default.

The reason for the different release name format for stable builds is
both to separate them, but also to make it easier to keep the version of
the homebrew cask as simply "27.2".
2021-07-03 00:11:59 +01:00
3f1059940d chore(release): 0.6.7 2021-07-02 16:50:25 +01:00
5c722e36c5 feat(bundle): move bundled shared libraries to Contents/Frameworks
The Frameworks folder is the recommended location to store shared
libraries within macOS application bundles. Previously we stored them in
Contents/MacOS/lib.

Latest nightly builds already store all *.eln files under the Frameworks
folder, so it seemed like a good time to make the change with the
library bundler/embedder too.
2021-07-02 16:49:32 +01:00
f52dd8dc6d chore(release): 0.6.6 2021-07-01 23:52:50 +01:00
4cdbaf1ec0 chore(cask): fix cask vs formula terminology
Homebrew casks are their own thing, separate from formulas. Hence remove
all mentions to "formula" to avoid confusion.
2021-07-01 23:50:50 +01:00
f3a289b11c chore(release): 0.6.5 2021-07-01 23:37:21 +01:00
9019e73d60 fix(native_comp): improve handling of *.eln files in .app bundle
Specifically support latest changes in master which places *.eln files
within the .app bundle in "Contents/Frameworks".
2021-07-01 23:33:50 +01:00
28930381a8 chore(release): 0.6.4 2021-06-30 10:41:34 +01:00
df25e54ef7 chore(sign): simplify *.eln locating logic
Instead of only checking very specific paths within the .app bundle,
just check the whole bundle for any and all *.eln files.
2021-06-30 10:20:22 +01:00
6d21d1bef4 chore(release): 0.6.3 2021-06-29 14:40:18 +01:00
99aa76b398 fix(patches): correctly set ref when loading a build plan YAML
The ref was not correctly set when loading a build plan, resulting in
the set of patches being selected were always for Emacs 28.x, preventing
builds of Emacs 27.x and 26.x.
2021-06-29 14:37:43 +01:00
b60ca528f8 chore(release): 0.6.2 2021-06-29 01:30:19 +01:00
23b8236e0a fix(native_comp): patch Emacs.pdmp for customized native-lisp paths
In my initial testing without full native-comp AoT, Emacs seemed to
somehow launch fine. But with a AoT build it complains that it can't
find *.eln files in the original paths that contained dots. But since we
have to customize those folder names removing the dots to make Apple's
codesign happy, we also need to update Emacs.pdmp too.
2021-06-29 01:27:16 +01:00
56d0364099 chore(release): 0.6.1 2021-06-28 22:50:20 +01:00
6af597b427 fix(cask): add missing --force flag to cask update command 2021-06-28 22:49:07 +01:00
a331457e89 chore(release): 0.6.0 2021-06-28 22:23:44 +01:00
a4171555f5 Merge pull request #52 from jimeh/cask-formula-management
feat(cask): add cask update command to manage cask formula
2021-06-28 22:20:39 +01:00
adbcfc6fc4 feat(cask): add cask update command to manage cask formula
This will be used by the jimeh/homebrew-emacs-builds brew tap repository
in combination with brew livecheck to automatically update cask formulas
to the latest nightly builds from the jimeh/emacs-builds repository.
2021-06-28 22:19:10 +01:00
634861beea chore(release): 0.5.2 2021-06-27 12:33:47 +01:00
eeca7b798d fix(native_comp): rename native-lisp folder paths to appease Apple's codesign
Apple's codesign CLI tool will throw an error when signing application
bundles, if any folder within the app's Contents/MacOS folder contains
two dots.

The recent relocation of the native-lisp folder from
Contents/Resources/native-lisp to
Contents/MacOS/lib/emacs/28.0.50/native-lisp is causing code signing to
fail.

The workaround here simply replaces dots (.) with hyphens (-), causing
the following folder renames:

    Contents/MacOS/lib/emacs/28.0.50/native-lisp/28.0.50-852ecda2 --> Contents/MacOS/lib/emacs/28.0.50/native-lisp/28-0-50-852ecda2
    Contents/MacOS/lib/emacs/28.0.50 --> Contents/MacOS/lib/emacs/28-0-50

To ensure Emacs can still find the bundled native-lisp files, we use a
symlink:

    Contents/native-lisp -> MacOS/lib/emacs/28-0-50/native-lisp

This type of fix is not ideal, but its the only way I know of getting
around this issue right now.

And we're already doing a similar thing for the embedded gcc libraries.
2021-06-27 12:25:35 +01:00
e1500cbf53 chore(release): 0.5.1 2021-06-27 02:32:38 +01:00
ca73ab7202 fix(native_comp): symlink creation was missing a conditional check
Latest commits to master as of June 26, 2021 have moved *.eln files back
to: MacOS/lib/emacs/**/native-lisp

Even though the symlinks are most likely no longer needed, I don't know
of an easy way to determine if they're required or not, so for now,
they'll be created to ensure older native-comp branch SHAs can still be
built.
2021-06-27 02:27:24 +01:00
fd6cd42d5c chore(release): 0.5.0 2021-06-22 00:23:43 +01:00
b719437bee chore(release)!: add v prefix to git version tags
BREAKING CHANGE: Add v prefix to git version tags
2021-06-22 00:21:34 +01:00
368b9271ff Merge pull request #44 from jimeh/sign-package-and-notarize
Sign, Package and Notarize
2021-06-22 00:16:50 +01:00
87d2d67575 chore(makefile): add bootstrap targets for normal use and CI environments 2021-06-22 00:13:25 +01:00
72ca9ce2b6 feat(release): add release publish command 2021-06-22 00:13:25 +01:00
276a9da5ee feat(release): add release check command 2021-06-22 00:13:24 +01:00
87ecfbcec0 feat(package): add package command to create a styled *.dmg for Emacs.app 2021-06-22 00:13:21 +01:00
55f35e1146 chore(package): add dmgbuild helper package to execute dmgbuild 2021-06-22 00:08:42 +01:00
72d0254772 feat(notarize): add notarize command to notarize and staple *.dmg files 2021-06-22 00:08:42 +01:00
698756ac55 feat(sign): add sign command to sign Emacs.app bundles with codesign
The sign command signs Emacs.app application bundles with Apple's
codesign utility.

It does a few things outside of just executing codesign:

- Is aware of *.eln native-compilation files, which need to be
  explicitly searched for on disk and passed to codesign, as they are
  not detected when using the "--deep" option.
- Is aware of Contents/MacOS/bin/emacs CLI helper tool which we add into
  the application bundle, and specifically passed it to codesign as
  well.
- By default provides a set of entitlements which are relevant for Emacs
  when running codesign.
2021-06-22 00:08:36 +01:00
1ffd735c23 feat(plan): add plan command to create build plans 2021-06-20 18:38:36 +01:00
8d87c01db7 feat(cli): add basis for new "emacs-builder" CLI tool written in Go
This will eventually replace the build-emacs-for-macos script that's
written in Ruby. The migration of functionality will happen bit by bit
over time.
2021-06-20 18:38:36 +01:00
3e1eb01e24 feat(build)!: add ability to output as directory and/or archive
This changes the filename pattern of the resulting archive, for example:

Old:

    Emacs.app-[master][2021-05-19][6ae3f7e][macOS-11.3][x86_64].tbz
New:

    Emacs.2021-05-19.6ae3f7e.master.macOS-11-3.x86_64.tbz

Emacs.app also resides within a folder in the archive now instead of the
in the root. For the above example, the path to Emacs.app within the
archive would be:

    Emacs.2021-05-19.6ae3f7e.master.macOS-11-3.x86_64/Emacs.app

Archive creation can also be skipped by passing in --no-archive, which
will instead leave a build folder with the same name as the archive.

If you want to keep the build directory, and also a create archive, pass
in --archive-keep-build-dir.

BREAKING CHANGE: New archive naming convention, and folder structure
within archive.
2021-06-20 18:38:36 +01:00
d7c59ba4b2 docs(readme): update CLI usage section 2021-06-20 18:38:10 +01:00
8f5ae8a6c2 chore(release): 0.4.17 2021-06-20 18:26:37 +01:00
707bc9e0d1 fix(download): don't use GitHub API to get tarball URL
The API tarball endpoint is subject to API rate limits, while the
non-API tarball endpoint is not. This should reduce the risk of rate
limit errors for people who don't have a GITHUB_TOKEN environment
variable set.
2021-06-20 17:52:17 +01:00
c55bf68e61 chore(release): 0.4.16 2021-06-07 23:42:18 +01:00
26ce446226 Merge pull request #49 from jimeh/fix-portability
fix: improve portability of builds
2021-06-07 23:31:30 +01:00
ca2d4c38f6 fix(compiling): improve portability of builds
This makes the -march=native CFLAG optional, and disabled by default,
but still available through a new --native-march flag.

It should make builds more portable between machines, as previously it
was very common to get a CPU architecture error on launch if you moved
the build to a different machine running a different generation of a
Intel CPU.

From what I've understood, when using the -march=native CFLAG clang will
make as many optimizations possible based on the exact set of CPU
instructions available on the specific CPU it's compiling on.

In theory this leads to a more optimized build, though I haven't
personally noticed any difference. But it also leads to less portable
builds, for example builds from a Intel-based 2020 MacBook Pro just
crash with a unsupported CPU architecture error when run on a
Intel-based 2016 MacBook Pro.
2021-06-07 23:25:42 +01:00
bd81870659 fix(native_comp): crash on launch when gcc homebrew package was not installed
It turns out all *.eln files link against the libgcc dylib in homebrew:

    /usr/local/lib/gcc/11/libgcc_s.1.dylib

So here we find all *.eln files in all relevant paths they may be
depending on how old of a native-comp source tree we're building, and
copy in any shared libs that resides within the homebrew prefix. Just
like we do for all other binaries that we copy libs for.
2021-06-07 23:21:29 +01:00
5cd9d1c5fa chore(release): 0.4.15 2021-05-31 23:25:03 +01:00
6ae14b597c Merge pull request #48 from jimeh/github-auth
feat(github): perform authenticated GitHub API requests when GITHUB_TOKEN env var is set
2021-05-31 23:23:24 +01:00
deda28e5ad feat(github): perform authenticated GitHub API requests when GITHUB_TOKEN env var is set
This should let people on shared connections use the script if they
have a GitHub Personal Access Token available in the GITHUB_TOKEN
environment variable.

When making unauthenticated API requests to GitHub, requests are rate
limited to 60 requests per hour based on source IP address. Hence on
shared connections the rate limit may easily be exceeded.

When making authenticated API requests to GitHub, up to 5000 requests
per hour is allowed, based on the authenticated user rather than source
IP address.
2021-05-31 23:14:54 +01:00
3d6caac962 chore(release): 0.4.14 2021-05-22 21:09:33 +01:00
11de27358d Merge pull request #43 from jimeh/improve-cli-integration
feat(cli): add "emacs" CLI launcher script to Emacs.app/Conents/MacOS/bin
2021-05-22 20:56:41 +01:00
8237aa9272 feat(cli): add "emacs" CLI launcher script to Emacs.app/Conents/MacOS/bin
This makes setting up a "emacs" terminal command that works with the
self-contained Emacs.app bundle much simpler, as you just need to add
Emacs.app/Conents/MacOS/bin to your PATH.

For example, if you place Emacs.app in /Applications, add this to your
shell setup:

    if [ -d "/Applications/Emacs.app/Contents/MacOS/bin" ]; then
      export PATH="/Applications/Emacs.app/Contents/MacOS/bin:$PATH"
      alias emacs="emacs -nw" # Always launch "emacs" in terminal mode.
    fi

The launcher script works by figuring out it's own absolute path on
disk, even if you are using a symlink to the script, it will resolve to
correct real path. This allows it to execute the main
Emacs.app/Contents/MacOS/Emacs executable via the correct path, so it
can correctly pick up its dependencies from within the Emacs.app bundle.

Fixes #41
2021-05-22 20:55:42 +01:00
3ffe83c94e docs(native_comp): update readme to reflect recent changes to native-comp 2021-05-22 20:32:51 +01:00
58 changed files with 6017 additions and 91 deletions

52
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
---
name: CI
on: [push]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: v1.40
env:
VERBOSE: "true"
tidy:
name: Tidy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: 1.16
- uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Check if mods are tidy
run: make check-tidy
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: 1.16
- uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Run tests
run: make test
env:
VERBOSE: "true"

2
.gitignore vendored
View File

@@ -1,6 +1,8 @@
.DS_Store
.envrc
Formula/*
Gemfile.lock
bin
builds
sources
tarballs

86
.golangci.yml Normal file
View File

@@ -0,0 +1,86 @@
linters-settings:
funlen:
lines: 100
statements: 150
goconst:
min-occurrences: 5
gocyclo:
min-complexity: 20
govet:
check-shadowing: true
enable-all: true
disable:
- fieldalignment
lll:
line-length: 80
tab-width: 4
maligned:
suggest-new: true
misspell:
locale: US
linters:
disable-all: true
enable:
- bodyclose
- deadcode
- depguard
- dupl
- errcheck
- exportloopref
- funlen
- gochecknoinits
- goconst
- gocritic
- gocyclo
- gofumpt
- goimports
- goprintffuncname
- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
- lll
- misspell
- nakedret
- nlreturn
- noctx
- nolintlint
- revive
- sqlclosecheck
- staticcheck
- structcheck
- typecheck
- unconvert
- unused
- varcheck
- whitespace
issues:
include:
# - EXC0002 # disable excluding of issues about comments from golint
exclude:
- Using the variable on range scope `tt` in function literal
- Using the variable on range scope `tc` in function literal
exclude-rules:
- path: "_test\\.go"
linters:
- funlen
- dupl
- goconst
- source: "^//go:generate "
linters:
- lll
- source: "`json:"
linters:
- lll
run:
skip-dirs:
- builds
- sources
- tarballs
timeout: 2m
allow-parallel-runners: true
modules-download-mode: readonly

View File

@@ -1,3 +1 @@
module.exports = {
tagPrefix: "",
};
module.exports = {};

3
Brewfile.ci Normal file
View File

@@ -0,0 +1,3 @@
# frozen_string_literal: true
brew 'python'

View File

@@ -2,6 +2,150 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.6.11](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.10...v0.6.11) (2021-07-17)
### Bug Fixes
* **native-comp:** fix re-linking and signing issue with *.eln files ([b03343f](https://github.com/jimeh/build-emacs-for-macos/commit/b03343f506aa3ceabdfa03f8a2916b2db4873f3f))
### [0.6.10](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.9...v0.6.10) (2021-07-16)
### Bug Fixes
* **native-comp:** *.eln files were not being found during shared lib embedding ([9d32509](https://github.com/jimeh/build-emacs-for-macos/commit/9d32509c615076618957cc47c82f6e9d8f972fe7))
### [0.6.9](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.8...v0.6.9) (2021-07-04)
### Features
* **release:** add bulk edit command to quickly change multiple GitHub releases ([cb63806](https://github.com/jimeh/build-emacs-for-macos/commit/cb638062625d9bc3eee12515067fb09e05a08414))
### Bug Fixes
* **plan:** correctly parse --test-release-type flag ([fd0ec4d](https://github.com/jimeh/build-emacs-for-macos/commit/fd0ec4d772dd3da93afc234fb3024220b2099c88))
### [0.6.8](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.7...v0.6.8) (2021-07-02)
### Features
* **builds:** add support for stable builds ([f4d6e3a](https://github.com/jimeh/build-emacs-for-macos/commit/f4d6e3a56d2c15b0c86af18e8d16bebbeb92a8ab))
### [0.6.7](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.6...v0.6.7) (2021-07-02)
### Features
* **bundle:** move bundled shared libraries to Contents/Frameworks ([5c722e3](https://github.com/jimeh/build-emacs-for-macos/commit/5c722e36c571aa7bf558b7f210c011f12d8d8a1c))
### [0.6.6](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.5...v0.6.6) (2021-07-01)
### [0.6.5](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.4...v0.6.5) (2021-07-01)
### Bug Fixes
* **native_comp:** improve handling of *.eln files in .app bundle ([9019e73](https://github.com/jimeh/build-emacs-for-macos/commit/9019e73d606f0379f988f46d6008770f8f3f7a51))
### [0.6.4](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.3...v0.6.4) (2021-06-30)
### [0.6.3](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.2...v0.6.3) (2021-06-29)
### Bug Fixes
* **patches:** correctly set ref when loading a build plan YAML ([99aa76b](https://github.com/jimeh/build-emacs-for-macos/commit/99aa76b3985195c310a20bafa19a8c7a4c8558fd))
### [0.6.2](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.1...v0.6.2) (2021-06-29)
### Bug Fixes
* **native_comp:** patch Emacs.pdmp for customized native-lisp paths ([23b8236](https://github.com/jimeh/build-emacs-for-macos/commit/23b8236e0a66fb09810e8422bedf02f7192a53e4))
### [0.6.1](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.0...v0.6.1) (2021-06-28)
### Bug Fixes
* **cask:** add missing --force flag to cask update command ([6af597b](https://github.com/jimeh/build-emacs-for-macos/commit/6af597b4271341f9796c3d9c356de9918e0f6f85))
## [0.6.0](https://github.com/jimeh/build-emacs-for-macos/compare/v0.5.2...v0.6.0) (2021-06-28)
### Features
* **cask:** add cask update command to manage cask formula ([adbcfc6](https://github.com/jimeh/build-emacs-for-macos/commit/adbcfc6fc433fcc99b10dc5ccb51ba458333fa9c))
### [0.5.2](https://github.com/jimeh/build-emacs-for-macos/compare/v0.5.1...v0.5.2) (2021-06-27)
### Bug Fixes
* **native_comp:** rename native-lisp folder paths to appease Apple's codesign ([eeca7b7](https://github.com/jimeh/build-emacs-for-macos/commit/eeca7b798de236a3ffc1ab04b0f7735a37ce5af4))
### [0.5.1](https://github.com/jimeh/build-emacs-for-macos/compare/v0.5.0...v0.5.1) (2021-06-27)
### Bug Fixes
* **native_comp:** symlink creation was missing a conditional check ([ca73ab7](https://github.com/jimeh/build-emacs-for-macos/commit/ca73ab7202877acefd97289f3d28e7c025e36b9d))
## [0.5.0](https://github.com/jimeh/build-emacs-for-macos/compare/v0.4.17...v0.5.0) (2021-06-21)
### ⚠ BREAKING CHANGES
* **release:** Add v prefix to git version tags
* **build:** New archive naming convention, and folder structure within archive.
### Features
* **build:** add ability to output as directory and/or archive ([3e1eb01](https://github.com/jimeh/build-emacs-for-macos/commit/3e1eb01e248ebbc314b8b9f50bbc371ac8df666b))
* **cli:** add basis for new "emacs-builder" CLI tool written in Go ([8d87c01](https://github.com/jimeh/build-emacs-for-macos/commit/8d87c01db79201182fbcd1a210b1b19df9209aeb))
* **notarize:** add notarize command to notarize and staple *.dmg files ([72d0254](https://github.com/jimeh/build-emacs-for-macos/commit/72d0254772bf7d0937b41634e9a4bfcf87f60fb6))
* **package:** add package command to create a styled *.dmg for Emacs.app ([87ecfbc](https://github.com/jimeh/build-emacs-for-macos/commit/87ecfbcec05b46d7a30202269474612834b648f3))
* **plan:** add plan command to create build plans ([1ffd735](https://github.com/jimeh/build-emacs-for-macos/commit/1ffd735c23e375479ea6bb2c771553ce4cac902b))
* **release:** add release check command ([276a9da](https://github.com/jimeh/build-emacs-for-macos/commit/276a9da5eed618322e09fba11a486ae0d9925fdd))
* **release:** add release publish command ([72ca9ce](https://github.com/jimeh/build-emacs-for-macos/commit/72ca9ce2b64505a8bbc50b3139c0f84fb24813fd))
* **sign:** add sign command to sign Emacs.app bundles with codesign ([698756a](https://github.com/jimeh/build-emacs-for-macos/commit/698756ac5597d3dc7b69f28bc209093fc8c11f30))
* **release:** add v prefix to git version tags ([b719437](https://github.com/jimeh/build-emacs-for-macos/commit/b719437bee9acf28d5d352eb44cbf4d3a17107d7))
### [0.4.17](https://github.com/jimeh/build-emacs-for-macos/compare/0.4.16...0.4.17) (2021-06-20)
### Bug Fixes
* **download:** don't use GitHub API to get tarball URL ([707bc9e](https://github.com/jimeh/build-emacs-for-macos/commit/707bc9e0d13246b7cfb8d27da859a101d4a3c166))
### [0.4.16](https://github.com/jimeh/build-emacs-for-macos/compare/0.4.15...0.4.16) (2021-06-07)
### Bug Fixes
* **compiling:** improve portability of builds ([ca2d4c3](https://github.com/jimeh/build-emacs-for-macos/commit/ca2d4c38f69c434c77c266594104bfbf34ad5221))
* **native_comp:** crash on launch when gcc homebrew package was not installed ([bd81870](https://github.com/jimeh/build-emacs-for-macos/commit/bd8187065928b9f79de8b14222c98f8dc34bfe5f))
### [0.4.15](https://github.com/jimeh/build-emacs-for-macos/compare/0.4.14...0.4.15) (2021-05-31)
### Features
* **github:** perform authenticated GitHub API requests when GITHUB_TOKEN env var is set ([deda28e](https://github.com/jimeh/build-emacs-for-macos/commit/deda28e5aded2817bcc7956f377378576372816f))
### [0.4.14](https://github.com/jimeh/build-emacs-for-macos/compare/0.4.13...0.4.14) (2021-05-22)
### Features
* **cli:** add "emacs" CLI launcher script to Emacs.app/Conents/MacOS/bin ([8237aa9](https://github.com/jimeh/build-emacs-for-macos/commit/8237aa9272ce1d13a412b2495cbaa90df38d928b)), closes [#41](https://github.com/jimeh/build-emacs-for-macos/issues/41)
### [0.4.13](https://github.com/jimeh/build-emacs-for-macos/compare/0.4.12...0.4.13) (2021-05-22)

184
Makefile
View File

@@ -1,3 +1,187 @@
PIP := $(shell command -v pip3 || command -v pip)
SOURCES := $(shell \
find * \
-not -path 'sources/*' \
-not -path 'builds/*' \( \
-name "*.go" -or \
-name "go.mod" -or \
-name "go.sum" -or \
-name "Makefile" -or \
-type f -path 'internal/*' -or \
-type f -path 'cmd/*' -or \
-type f -path 'pkg/*' \
\) | grep -v '.DS_Store' \
)
#
# Environment
#
# Verbose output
ifdef VERBOSE
V = -v
endif
BINDIR := bin
TOOLDIR := $(BINDIR)/tools
# Global environment variables for all targets
SHELL ?= /bin/bash
SHELL := env \
GO111MODULE=on \
GOBIN=$(CURDIR)/$(BINDIR) \
CGO_ENABLED=0 \
PATH='$(CURDIR)/$(BINDIR):$(CURDIR)/$(TOOLDIR):$(PATH)' \
$(SHELL)
#
# Defaults
#
# Default target
.DEFAULT_GOAL := build
#
# Bootstrap
#
bootstrap: bootstrap-brew
bootstrap-ci: bootstrap-brew bootstrap-brew-ci bootstrap-pip
bootstrap-brew:
brew bundle
bootstrap-brew-ci:
brew bundle --file Brewfile.ci
bootstrap-pip:
$(PIP) install -r requirements-ci.txt
#
# Tools
#
# external tool
define tool # 1: binary-name, 2: go-import-path
TOOLS += $(TOOLDIR)/$(1)
$(TOOLDIR)/$(1): Makefile
GOBIN="$(CURDIR)/$(TOOLDIR)" go install "$(2)"
endef
$(eval $(call tool,gofumpt,mvdan.cc/gofumpt@latest))
$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.40))
$(eval $(call tool,gomod,github.com/Helcaraxan/gomod@latest))
.PHONY: tools
tools: $(TOOLS)
#
# Build
#
LDFLAGS := -w -s
VERSION ?= $(shell git describe --tags --exact 2>/dev/null)
COMMIT ?= $(shell git rev-parse HEAD 2>/dev/null)
DATE ?= $(shell date '+%FT%T%z')
ifeq ($(VERSION),)
VERSION = 0.0.0-dev
endif
CMDDIR := cmd
BINS := $(shell test -d "$(CMDDIR)" && cd "$(CMDDIR)" && \
find * -maxdepth 0 -type d -exec echo $(BINDIR)/{} \;)
.PHONY: build
build: $(BINS)
$(BINS): $(BINDIR)/%: $(SOURCES)
mkdir -p "$(BINDIR)"
cd "$(CMDDIR)/$*" && go build -a $(V) \
-o "$(CURDIR)/$(BINDIR)/$*" \
-ldflags "$(LDFLAGS) \
-X main.version=$(VERSION) \
-X main.commit=$(COMMIT) \
-X main.date=$(DATE)"
#
# Development
#
TEST ?= $$(go list ./... | grep -v 'sources/' | grep -v 'builds/')
.PHONY: clean
clean:
rm -rf $(BINARY) $(TOOLS)
rm -f ./go.mod.tidy-check ./go.sum.tidy-check
.PHONY: test
test:
CGO_ENABLED=1 go test $(V) -count=1 -race $(TESTARGS) $(TEST)
.PHONY: lint
lint: $(TOOLDIR)/golangci-lint
golangci-lint $(V) run
.PHONY: format
format: $(TOOLDIR)/gofumpt
gofumpt -w .
.PHONY: gen
gen:
go generate $$(go list ./... | grep -v 'sources/' | grep -v 'builds/')
#
# Dependencies
#
.PHONY: deps
deps:
$(info Downloading dependencies)
go mod download
.PHONY: deps-update
deps-update:
$(info Downloading dependencies)
go get -u -t ./...
.PHONY: deps-analyze
deps-analyze: $(TOOLDIR)/gomod
gomod analyze
.PHONY: tidy
tidy:
go mod tidy $(V)
.PHONY: verify
verify:
go mod verify
.SILENT: check-tidy
.PHONY: check-tidy
check-tidy:
cp go.mod go.mod.tidy-check
cp go.sum go.sum.tidy-check
go mod tidy
( \
diff go.mod go.mod.tidy-check && \
diff go.sum go.sum.tidy-check && \
rm -f go.mod go.sum && \
mv go.mod.tidy-check go.mod && \
mv go.sum.tidy-check go.sum \
) || ( \
rm -f go.mod go.sum && \
mv go.mod.tidy-check go.mod && \
mv go.sum.tidy-check go.sum; \
exit 1 \
)
#
# Release
#
.PHONY: new-version
new-version: check-npx
npx standard-version

View File

@@ -45,6 +45,11 @@ The build produced does have some limitations:
that built the application will yield warnings. If you want to make a signed
Emacs.app, google is you friend for finding signing instructions.
## Binary Builds
Nightly and stable binary builds produced with this build script are available
from [jimeh/emacs-builds](https://github.com/jimeh/emacs-builds).
## Requirements
- [Xcode](https://apps.apple.com/gb/app/xcode/id497799835?mt=12)
@@ -74,11 +79,18 @@ Options:
--git-sha SHA Override detected git SHA of specified branch allowing builds of old commits
--[no-]xwidgets Enable/disable XWidgets if supported (default: enabled)
--[no-]native-comp Enable/disable native-comp (default: enabled if supported)
--[no-]native-march Enable/disable -march=native CFLAG(default: disabled)
--[no-]native-full-aot Enable/disable NATIVE_FULL_AOT / Ahead of Time compilation (default: disabled)
--[no-]rsvg Enable/disable SVG image support via librsvg (default: enabled)
--no-titlebar Apply no-titlebar patch (default: disabled)
--no-frame-refocus Apply no-frame-refocus patch (default: disabled)
--[no-]github-auth Make authenticated GitHub API requests if GITHUB_TOKEN environment variable is set.(default: enabled)
--work-dir DIR Specify a working directory where tarballs, sources, and builds will be stored and worked with
-o, --output DIR Output directory for finished builds (default: <work-dir>/builds)
--build-name NAME Override generated build name
--dist-include x,y,z List of extra files to copy from Emacs source into build folder/archive (default: COPYING)
--[no-]archive Enable/disable creating *.tbz archive (default: enabled)
--[no-]archive-keep Enable/disable keeping source folder for archive (default: disabled)
--plan FILE Follow given plan file, instead of using given git ref/sha
```
@@ -111,29 +123,28 @@ All sources as downloaded as tarballs from the
to get a list of tags/branches available to install, simply check said
repository.
## Use Self-Contained Emacs.app as `emacs` CLI Tool
## Use Emacs.app as `emacs` CLI Tool
As the application bundle is self-contained, the main executable needs to be run
from within the application bundle. This means a simple symlink to
`Emacs.app/Contents/MacOS/Emacs` will not work. Instead the best approach is to
create a shell alias called `emacs` pointing to the right place.
Builds come with a custom `emacs` shell script launcher for use from the command
line, located next to `emacsclient` in `Emacs.app/Contents/MacOS/bin`.
Personally I use something similar to this:
The custom `emacs` script makes sure to use the main
`Emacs.app/Contents/MacOS/Emacs` executable from the correct path, ensuring it
finds all the relevant dependencies within the Emacs.app bundle, regardless of
it it's exposed via `PATH` or symlinked to from elsewhere.
To use it, simply add `Emacs.app/Contents/MacOS/bin` to your `PATH`. For
example, if you place Emacs.app in `/Applications`:
```bash
if [ -f "/Applications/Emacs.app/Contents/MacOS/Emacs" ]; then
export EMACS="/Applications/Emacs.app/Contents/MacOS/Emacs"
alias emacs="$EMACS -nw"
fi
if [ -f "/Applications/Emacs.app/Contents/MacOS/bin/emacsclient" ]; then
alias emacsclient="/Applications/Emacs.app/Contents/MacOS/bin/emacsclient"
if [ -d "/Applications/Emacs.app/Contents/MacOS/bin" ]; then
export PATH="/Applications/Emacs.app/Contents/MacOS/bin:$PATH"
alias emacs="emacs -nw" # Always launch "emacs" in terminal mode.
fi
```
Setting the `EMACS` variable to the binary path seems to be a good idea, as some
tools seems to use it to figure out the path to Emacs' executable, including
[doom-emacs](https://github.com/hlissner/doom-emacs)' `doom` CLI tool.
If you want `emacs` in your terminal to launch a GUI instance of Emacs, don't
use the alias from the above example.
## Native-Comp
@@ -142,9 +153,9 @@ _Note: On 2021-04-25 the `feature/native-comp` branch was
into `master`._
The build script will automatically detect if the source tree being built
supports native-compilation, and enable it if available. You can override this
to force it on/off by passing `--native-comp` or `--no-native-comp`
respectfully.
supports native-compilation, and enable it if available. You can override the
auto-detection logic to force enable or force disable native-compilation by
passing `--native-comp` or `--no-native-comp` respectfully.
By default `NATIVE_FULL_AOT` is disabled which ensures a fast build by native
compiling as few elisp source files as possible to build Emacs itself. Any
@@ -152,32 +163,39 @@ remaining elisp files will be dynamically compiled in the background the first
time they are used.
To enable native full Ahead-of-Time compilation, pass in the `--native-full-aot`
option, which will native-compile all of Emacs' elisp as built-time. On my
option, which will native-compile all of Emacs' elisp at built-time. On my
machine it takes around 10 minutes to build Emacs.app with `NATIVE_FULL_AOT`
disabled, and around 20-25 minutes with it enabled.
### Configuration
Add the following near the top of your `early-init.el` or `init.el`:
```elisp
(setq comp-speed 2)
```
#### Native-Lisp Cache Directory
By default natively compiled `*.eln` files will be cached in
`~/.emacs.d/eln-cache/`. If you want to customize that, simply set a new path as
the first element of the `comp-eln-load-path` variable. The path string must end
with a `/`.
the first element of the `native-comp-eln-load-path` variable. The path string
must end with a `/`.
Below is an example which stores all compiled `*.eln` files in `cache/eln-cache`
within your Emacs configuration directory:
```elisp
(when (boundp 'comp-eln-load-path)
(setcar comp-eln-load-path
(when (boundp 'native-comp-eln-load-path)
(setcar native-comp-eln-load-path
(expand-file-name "cache/eln-cache/" user-emacs-directory)))
```
#### Compilation Warnings
By default any warnings encountered during async native compilation will pop up
a warnings buffer. As this tends to happen rather frequently with a lot of
packages, it can get annoying. You can disable showing these warnings by setting
`native-comp-async-report-warnings-errors` to `nil`:
```elisp
(setq native-comp-async-report-warnings-errors nil)
```
### Issues
Please see all issues with the
@@ -188,7 +206,7 @@ types of issues and or behavior you can expect.
### Known Good Commits/Builds
A list of known "good" commits which produce working builds is tracked in:
[#6 Known good commits of feature/native-comp branch](https://github.com/jimeh/build-emacs-for-macos/issues/6)
[#6 Known good commits for native-comp](https://github.com/jimeh/build-emacs-for-macos/issues/6)
## Credits

View File

@@ -11,6 +11,7 @@ require 'net/http'
require 'optparse'
require 'pathname'
require 'time'
require 'tmpdir'
require 'uri'
require 'yaml'
@@ -71,9 +72,8 @@ end
class Build
include Output
EMACS_MIRROR_REPO = 'emacs-mirror/emacs'
DOWNLOAD_URL = 'https://github.com/emacs-mirror/emacs/tarball/%s'
LATEST_URL = 'https://api.github.com/repos/emacs-mirror/emacs/commits/%s'
NATIVE_COMP_REF_REGEXP = %r{^feature/native-comp}.freeze
attr_reader :root_dir
attr_reader :source_dir
@@ -102,12 +102,15 @@ class Build
detect_native_comp if options[:native_comp].nil?
app = compile_source(@source_dir)
symlink_internals(app)
build_dir, app = create_build_dir(app)
handle_native_lisp(app)
add_cli_helper(app)
LibEmbedder.new(app, brew_dir, extra_libs).embed
GccLibEmbedder.new(app, gcc_info).embed if options[:native_comp]
archive_app(app)
archive_build(build_dir) if options[:archive]
end
private
@@ -115,12 +118,26 @@ class Build
def load_plan(filename)
plan = YAML.safe_load(File.read(filename), [:Time])
@ref = plan.dig('source', 'ref')
@meta = {
sha: plan.dig('commit', 'sha'),
ref: plan.dig('commit', 'ref'),
date: plan.dig('commit', 'date')
sha: plan.dig('source', 'commit', 'sha'),
ref: @ref,
date: plan.dig('source', 'commit', 'date')
}
@archive_filename = plan['archive']
if plan.dig('output', 'directory')
@output_dir = plan.dig('output', 'directory')
end
if plan.dig('output', 'archive')
@archive_filename = plan.dig('output', 'archive')
end
if plan.dig('output', 'disk_image') || !plan.dig('output', 'archive')
options[:archive] = false
end
@build_name = plan.dig('build', 'name') if plan.dig('build', 'name')
end
def tarballs_dir
@@ -131,8 +148,8 @@ class Build
@sources_dir ||= File.join(root_dir, 'sources')
end
def builds_dir
@builds_dir ||= File.join(root_dir, 'builds')
def output_dir
@output_dir ||= (options[:output] || File.join(root_dir, 'builds'))
end
def brew_dir
@@ -161,8 +178,20 @@ class Build
info 'Downloading tarball from GitHub. This could take a while, ' \
'please be patient.'
result = run_cmd('curl', '-L', url, '-o', target)
err 'Download failed.' unless result
args = ['curl', '-L', url, '-o', target]
log_args = args.clone
if options[:github_auth] && ENV['GITHUB_TOKEN']
args = [args[0]] +
['-H', "Authorization: Token #{ENV['GITHUB_TOKEN']}"] + args[1..-1]
log_args = [log_args[0]] +
['-H', '"Authorization: Token $GITHUB_TOKEN"'] +
log_args[1..-1]
end
out "CMD: #{log_args.join(' ')}"
system(*args) || err("Exit code: #{$CHILD_STATUS.exitstatus}")
target
end
@@ -255,7 +284,7 @@ class Build
"-I#{File.join(gcc_info.root_dir, 'include')}",
"-I#{File.join(gcc_info.libgccjit_root_dir, 'include')}",
'-O2',
'-march=native',
(options[:native_march] ? '-march=native' : nil),
ENV['CFLAGS']
].compact.join(' ')
@@ -265,6 +294,8 @@ class Build
"-L#{gcc_info.libgccjit_lib_dir}",
"-I#{File.join(gcc_info.root_dir, 'include')}",
"-I#{File.join(gcc_info.libgccjit_root_dir, 'include')}",
# Ensure library re-linking and code signing will work after building.
'-Wl,-headerpad_max_install_names',
ENV['LDFLAGS']
].compact.join(' ')
@@ -351,61 +382,175 @@ class Build
emacs_app
end
def symlink_internals(app)
def create_build_dir(app)
app_name = File.basename(app)
target_dir = File.join(output_dir, build_name)
if File.exist?(target_dir)
err "Output directory #{target_dir} already exists, " \
'please delete it and try again'
end
info "Copying \"#{app_name}\" to: #{target_dir}"
FileUtils.mkdir_p(target_dir)
FileUtils.cp_r(app, target_dir)
options[:dist_include]&.each do |filename|
src = File.join(source_dir, filename)
if File.exist?(src)
info "Copying \"#{filename}\" to: #{target_dir}"
FileUtils.cp_r(src, target_dir)
else
info "Warning: #{filename} does not exist in #{source_dir}"
end
end
[target_dir, File.join(target_dir, File.basename(app))]
end
def handle_native_lisp(app)
return unless options[:native_comp]
FileUtils.cd(File.join(app, 'Contents')) do
# Skip creation of symlinks if *.eln files are located under
# Resources/native-lisp. Emacs is capable of finding lisp sources and
# *.eln cache files without symlinks.
return if Dir['Resources/native-lisp/**/*.eln'].any?
contents_dir = File.join(app, 'Contents')
FileUtils.cd(contents_dir) do
source = Dir['MacOS/libexec/emacs/**/eln-cache',
'MacOS/lib/emacs/**/native-lisp'].first
# Skip creation of symlinks if *.eln files are not located in a location
# known to be used by builds which need symlinks and other tweaks.
return if source.nil?
info 'Creating symlinks within Emacs.app needed for native-comp'
FileUtils.ln_s('Resources/lisp', 'lisp') unless File.exist?('lisp')
if !File.exist?('lisp') && File.exist?('Resources/lisp')
FileUtils.ln_s('Resources/lisp', 'lisp')
end
source = Dir['MacOS/libexec/emacs/**/eln-cache',
'MacOS/lib/emacs/**/native-lisp'].first
err 'Failed to find native-lisp cache directory for symlink creation.'
# Check for folder name containing two dots (.), as this causes Apple's
# codesign CLI tool to fail signing the Emacs.app bundle, complaining with
# q "bundle format unrecognized" error.
#
# The workaround for now is to rename the folder replacing the dots with
# hyphens (-), and create the native-lisp symlink pointing to the new
# location.
eln_dir = File.dirname(Dir[File.join(source, '**', '*.eln')].first)
if eln_dir.match(%r{/.+\..+\..+/})
base = File.basename(eln_dir)
parent = File.dirname(eln_dir)
until ['.', '/', contents_dir].include?(parent)
if base.match(/\..+\./)
old_name = File.join(parent, base)
new_name = File.join(parent, base.gsub(/\.(.+)\./, '-\\1-'))
info "Renaming: #{old_name} --> #{new_name}"
FileUtils.mv(old_name, new_name)
end
base = File.basename(parent)
parent = File.dirname(parent)
end
eln_parts = eln_dir.match(
%r{/(\d+\.\d+\.\d+)/native-lisp/(\d+\.\d+\.\d+-\w+)(?:/.+)?$}i
)
if eln_parts
patch_dump_native_lisp_paths(app, eln_parts[1], eln_parts[2])
end
# Find native-lisp directory again after it has been renamed.
source = Dir['MacOS/libexec/emacs/**/eln-cache',
'MacOS/lib/emacs/**/native-lisp'].first
if source.nil?
err 'Failed to find native-lisp cache directory for symlink creation.'
end
end
target = File.basename(source)
FileUtils.ln_s(source, target) unless File.exist?(target)
end
end
def archive_filename
return @archive_filename if @archive_filename
def patch_dump_native_lisp_paths(app, emacs_version, eln_version)
sanitized_emacs_version = emacs_version.gsub('.', '-')
sanitized_eln_version = eln_version.gsub('.', '-')
metadata = [
meta[:ref]&.gsub(/\W/, '-'),
meta[:date]&.strftime('%Y-%m-%d'),
meta[:sha][0..6],
"macOS-#{OS.version}",
OS.arch
].compact
contents_dir = File.join(app, 'Contents')
FileUtils.cd(contents_dir) do
filename = Dir['MacOS/Emacs.pdmp', 'MacOS/libexec/Emacs.pdmp'].first
err "no Emacs.pdmp file found in #{app}" unless filename
info 'patching Emacs.pdmp to point at new native-lisp paths'
filename = "Emacs.app-[#{metadata.join('][')}].tbz"
@archive_filename = File.join(builds_dir, filename)
content = File.read(filename, mode: 'rb').gsub(
"lib/emacs/#{emacs_version}/native-lisp/#{eln_version}/",
"lib/emacs/#{sanitized_emacs_version}/" \
"native-lisp/#{sanitized_eln_version}/"
).gsub(
"../native-lisp/#{eln_version}/",
"../native-lisp/#{sanitized_eln_version}/"
)
File.open(filename, 'w') { |f| f.write(content) }
end
end
def archive_app(app)
def add_cli_helper(app)
source = File.join(__dir__, 'helper', 'emacs-cli.bash')
target = File.join(app, 'Contents', 'MacOS', 'bin', 'emacs')
dir = File.dirname(target)
info "Adding \"emacs\" CLI helper to #{dir}"
FileUtils.mkdir_p(dir)
FileUtils.cp(source, target)
FileUtils.chmod('+w', target)
end
def build_name
return @build_name if @build_name
return @build_name = options[:build_name] if options[:build_name]
metadata = [
meta[:date]&.strftime('%Y-%m-%d'),
meta[:sha][0..6],
meta[:ref],
"macOS-#{OS.version}",
OS.arch
].compact.map { |v| v.gsub(/[^\w_-]+/, '-') }
@build_name = "Emacs.#{metadata.join('.')}"
end
def archive_filename
@archive_filename ||= File.join(output_dir, "#{build_name}.tbz")
end
def archive_build(build_dir)
filename = File.basename(archive_filename)
target_dir = File.dirname(archive_filename)
relative_target_dir = target_dir.gsub(root_dir + '/', '')
FileUtils.mkdir_p(target_dir)
app_base = File.basename(app)
app_dir = File.dirname(app)
build = File.basename(build_dir)
parent_dir = File.dirname(build_dir)
if !File.exist?(archive_filename)
info "Creating #{filename} archive in \"#{relative_target_dir}\"..."
FileUtils.cd(app_dir) do
system('tar', '-cjf', archive_filename, app_base)
info "Creating #{filename} archive in \"#{target_dir}\"..."
FileUtils.cd(parent_dir) do
system('tar', '-cjf', archive_filename, build)
if options[:archive_keep] == false
info "Removeing \"#{build}\" directory from #{parent_dir}"
FileUtils.rm_rf(build_dir)
end
end
else
info "#{filename} archive exists in " \
"#{relative_target_dir}, skipping archving."
"#{target_dir}, skipping archving."
end
end
@@ -429,8 +574,9 @@ class Build
ref_sha = options[:git_sha] || ref
info "Fetching info for git ref: #{ref_sha}"
url = format(LATEST_URL, ref_sha)
commit_json = http_get(url)
commit_json = github_api_get(
"/repos/#{EMACS_MIRROR_REPO}/commits/#{ref_sha}"
)
err "Failed to get commit info about: #{ref_sha}" if commit_json.nil?
commit = JSON.parse(commit_json)
@@ -443,8 +589,18 @@ class Build
@meta = meta
end
def http_get(url)
response = Net::HTTP.get_response(URI.parse(url))
def github_api_get(uri)
uri = URI.join('https://api.github.com/', uri)
http = Net::HTTP.new(uri.hostname, uri.port)
http.use_ssl = true if uri.scheme == 'https'
request = Net::HTTP::Get.new(uri)
if options[:github_auth] && ENV['GITHUB_TOKEN']
request['Authorization'] = "Token #{ENV['GITHUB_TOKEN']}"
end
response = http.request(request)
return unless response.code == '200'
response.body
@@ -582,8 +738,6 @@ class AbstractEmbedder
@app = app
end
private
def invocation_dir
File.join(app, 'Contents', 'MacOS')
end
@@ -593,7 +747,7 @@ class AbstractEmbedder
end
def lib_dir
File.join(invocation_dir, 'lib')
File.join(app, 'Contents', 'Frameworks')
end
end
@@ -617,11 +771,23 @@ class LibEmbedder < AbstractEmbedder
FileUtils.cd(File.dirname(app)) do
copy_libs(binary)
copy_extra_libs(extra_libs, binary) if extra_libs.any?
if eln_files.any?
info "Embedding libraries for #{eln_files.size} *.eln files " \
'within Emacs.app'
rel_path = Pathname.new(lib_dir).relative_path_from(
Pathname.new(File.dirname(binary))
).to_s
eln_files.each { |f| copy_libs(f, rel_path) }
end
end
end
private
def eln_files
@eln_files ||= Dir[File.join(app, 'Contents', '**', '*.eln')]
end
def copy_libs(exe, rel_path = nil)
exe_file = File.basename(exe)
rel_path ||= Pathname.new(lib_dir).relative_path_from(
@@ -725,15 +891,15 @@ class GccLibEmbedder < AbstractEmbedder
end
def target_dir
File.join(invocation_dir, gcc_info.relative_lib_dir)
File.join(lib_dir, gcc_info.relative_lib_dir)
end
def source_darwin_dir
File.join(invocation_dir, gcc_info.relative_darwin_lib_dir)
File.join(lib_dir, gcc_info.relative_darwin_lib_dir)
end
def target_darwin_dir
File.join(invocation_dir, gcc_info.sanitized_relative_darwin_lib_dir)
File.join(lib_dir, gcc_info.sanitized_relative_darwin_lib_dir)
end
def source_dir
@@ -789,7 +955,21 @@ class GccInfo
def sanitized_relative_darwin_lib_dir
@sanitized_relative_darwin_lib_dir ||= File.join(
File.dirname(relative_darwin_lib_dir),
File.basename(relative_darwin_lib_dir).split('.').first
File.basename(relative_darwin_lib_dir).gsub('.', '_')
)
end
def app_bundle_relative_lib_dir
@app_bundle_relative_lib_dir ||= relative_dir(
File.join(embedder.lib_dir, relative_lib_dir),
embedder.invocation_dir
)
end
def app_bundle_relative_darwin_lib_dir
@app_bundle_relative_darwin_lib_dir ||= relative_dir(
File.join(embedder.lib_dir, sanitized_relative_darwin_lib_dir),
embedder.invocation_dir
)
end
@@ -836,6 +1016,10 @@ class GccInfo
private
def embedder
@embedder ||= AbstractEmbedder.new(Dir.mktmpdir(['Emacs', '.app']))
end
def relative_dir(path, root)
Pathname.new(path).relative_path_from(Pathname.new(root)).to_s
end
@@ -845,9 +1029,14 @@ if __FILE__ == $PROGRAM_NAME
cli_options = {
work_dir: File.expand_path(__dir__),
native_full_aot: false,
native_march: false,
parallel: Etc.nprocessors,
rsvg: true,
xwidgets: true
xwidgets: true,
github_auth: true,
dist_include: ['COPYING'],
archive: true,
archive_keep: false
}
begin
@@ -885,6 +1074,12 @@ if __FILE__ == $PROGRAM_NAME
cli_options[:native_comp] = v
end
opts.on('--[no-]native-march',
'Enable/disable -march=native CFLAG' \
'(default: disabled)') do |v|
cli_options[:native_march] = v
end
opts.on('--[no-]native-full-aot',
'Enable/disable NATIVE_FULL_AOT / Ahead of Time compilation ' \
'(default: disabled)') do |v|
@@ -906,12 +1101,46 @@ if __FILE__ == $PROGRAM_NAME
cli_options[:no_frame_refocus] = true
end
opts.on('--[no-]github-auth',
'Make authenticated GitHub API requests if GITHUB_TOKEN ' \
'environment variable is set.' \
'(default: enabled)') do |v|
cli_options[:github_auth] = v
end
opts.on('--work-dir DIR',
'Specify a working directory where tarballs, sources, and ' \
'builds will be stored and worked with') do |v|
cli_options[:work_dir] = v
end
opts.on('-o DIR', '--output DIR',
'Output directory for finished builds ' \
'(default: <work-dir>/builds)') do |v|
cli_options[:output] = v
end
opts.on('--build-name NAME', 'Override generated build name') do |v|
cli_options[:build_name] = v
end
opts.on('--dist-include x,y,z',
'List of extra files to copy from Emacs source into build ' \
'folder/archive (default: COPYING)') do |v|
cli_options[:dist_include] = v
end
opts.on('--[no-]archive',
'Enable/disable creating *.tbz archive (default: enabled)') do |v|
cli_options[:archive] = v
end
opts.on('--[no-]archive-keep-build-dir',
'Enable/disable keeping source folder for archive ' \
'(default: disabled)') do |v|
cli_options[:archive_keep] = v
end
opts.on(
'--plan FILE',
'Follow given plan file, instead of using given git ref/sha'

24
cmd/emacs-builder/main.go Normal file
View File

@@ -0,0 +1,24 @@
package main
import (
"fmt"
"os"
"github.com/jimeh/build-emacs-for-macos/pkg/cli"
)
var (
version string
commit string
date string
)
func main() {
cliInstance := cli.New(version, commit, date)
err := cliInstance.Run(os.Args)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err.Error())
os.Exit(1)
}
}

30
go.mod Normal file
View File

@@ -0,0 +1,30 @@
module github.com/jimeh/build-emacs-for-macos
go 1.16
require (
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/fatih/color v1.12.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-github/v35 v35.3.0
github.com/google/go-querystring v1.1.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v0.16.1
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
github.com/hexops/gotextdiff v1.0.3
github.com/jimeh/undent v1.1.0
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/mitchellh/gon v0.2.3
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/stretchr/testify v1.7.0
github.com/urfave/cli/v2 v2.3.0
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
golang.org/x/oauth2 v0.0.0-20210615190721-d04028783cf1
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
howett.net/plist v0.0.0-20201203080718-1454fab16a06 // indirect
)

470
go.sum Normal file
View File

@@ -0,0 +1,470 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github/v35 v35.3.0 h1:fU+WBzuukn0VssbayTT+Zo3/ESKX9JYWjbZTLOTEyho=
github.com/google/go-github/v35 v35.3.0/go.mod h1:yWB7uCcVWaUbUP74Aq3whuMySRMatyRmq5U9FTNlbio=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v0.9.3-0.20191025211905-234833755cb2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v0.16.1 h1:IVQwpTGNRRIHafnTs2dQLIk4ENtneRIEEJWOVDqz99o=
github.com/hashicorp/go-hclog v0.16.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.6.3/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/go-retryablehttp v0.7.0 h1:eu1EI/mbirUgP5C8hVsTNaGZreBDlYiwC1FZWkvQPQ4=
github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jimeh/undent v1.1.0 h1:Cge7P4Ws6buy0SVuHBluY/aOKdFuJUMzoJswfAHZ4zE=
github.com/jimeh/undent v1.1.0/go.mod h1:oxYCIzdbyQNy8GXnCnjRJ2NS6Uq4p4yWoeawiGFqoHI=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/gon v0.2.3 h1:fObN7hD14VacGG++t27GzTW6opP0lwI7TsgTPL55wBo=
github.com/mitchellh/gon v0.2.3/go.mod h1:Ua18ZhqjZHg8VyqZo8kNHAY331ntV6nNJ9mT3s2mIo8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rhysd/go-fakeio v1.0.0 h1:+TjiKCOs32dONY7DaoVz/VPOdvRkPfBkEyUDIpM8FQY=
github.com/rhysd/go-fakeio v1.0.0/go.mod h1:joYxF906trVwp2JLrE4jlN7A0z6wrz8O6o1UjarbFzE=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sebdah/goldie v1.0.0 h1:9GNhIat69MSlz/ndaBg48vl9dF5fI+NBB6kfOxgfkMc=
github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210615190721-d04028783cf1 h1:x622Z2o4hgCr/4CiKWc51jHVKaWdtVpBNmEI8wI9Qns=
golang.org/x/oauth2 v0.0.0-20210615190721-d04028783cf1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
howett.net/plist v0.0.0-20201203080718-1454fab16a06 h1:QDxUo/w2COstK1wIBYpzQlHX/NqaQTcf9jyz347nI58=
howett.net/plist v0.0.0-20201203080718-1454fab16a06/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

23
helper/emacs-cli.bash Executable file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
resolve_link() {
"$(type -p greadlink readlink | head -1)" "$1"
}
abs_dirname() {
local path="$1"
local name
local cwd
cwd="$(pwd)"
while [ -n "$path" ]; do
cd "${path%/*}" || exit 1
name="${path##*/}"
path="$(resolve_link "$name" || true)"
done
pwd
cd "$cwd" || exit 1
}
exec "$(dirname "$(abs_dirname "$0")")/Emacs" "$@"

View File

@@ -1,8 +1,8 @@
diff --git a/lisp/emacs-lisp/comp.el b/lisp/emacs-lisp/comp.el
index 8c638312b0..87af889ef4 100644
index 638d4b274c..2599211936 100644
--- a/lisp/emacs-lisp/comp.el
+++ b/lisp/emacs-lisp/comp.el
@@ -4215,6 +4215,52 @@ native-compile-async
@@ -4224,6 +4224,52 @@ native-compile-async
(let ((load (not (not load))))
(native--compile-async files recursively load selector)))
@@ -16,10 +16,10 @@ index 8c638312b0..87af889ef4 100644
+ (devtools-dir
+ "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib")
+ (gcc-dir (expand-file-name
+ "<%= relative_lib_dir %>"
+ "<%= app_bundle_relative_lib_dir %>"
+ invocation-directory))
+ (darwin-dir (expand-file-name
+ "<%= sanitized_relative_darwin_lib_dir %>"
+ "<%= app_bundle_relative_darwin_lib_dir %>"
+ invocation-directory))
+ (lib-paths (list)))
+

13
pkg/cask/live_check.go Normal file
View File

@@ -0,0 +1,13 @@
package cask
type LiveCheck struct {
Cask string `json:"cask"`
Version LiveCheckVersion `json:"version"`
}
type LiveCheckVersion struct {
Current string `json:"current"`
Latest string `json:"latest"`
Outdated bool `json:"outdated"`
NewerThanUpstream bool `json:"newer_than_upstream"`
}

60
pkg/cask/release_info.go Normal file
View File

@@ -0,0 +1,60 @@
package cask
import (
"sort"
"strings"
)
type ReleaseInfo struct {
Name string
Version string
Assets map[string]*ReleaseAsset
}
func (s *ReleaseInfo) Asset(nameMatch string) *ReleaseAsset {
if a, ok := s.Assets[nameMatch]; ok {
return a
}
// Dirty and inefficient way to ensure assets are searched in a predictable
// order.
var assets []*ReleaseAsset
for _, a := range s.Assets {
assets = append(assets, a)
}
sort.SliceStable(assets, func(i, j int) bool {
return assets[i].Filename < assets[j].Filename
})
for _, a := range assets {
if strings.Contains(a.Filename, nameMatch) {
return a
}
}
return nil
}
func (s *ReleaseInfo) DownloadURL(nameMatch string) string {
a := s.Asset(nameMatch)
if a == nil {
return ""
}
return a.DownloadURL
}
func (s *ReleaseInfo) SHA256(nameMatch string) string {
a := s.Asset(nameMatch)
if a == nil {
return ""
}
return a.SHA256
}
type ReleaseAsset struct {
Filename string
DownloadURL string
SHA256 string
}

493
pkg/cask/update.go Normal file
View File

@@ -0,0 +1,493 @@
package cask
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"text/template"
"time"
"github.com/google/go-github/v35/github"
"github.com/hashicorp/go-hclog"
"github.com/hexops/gotextdiff"
"github.com/hexops/gotextdiff/myers"
"github.com/hexops/gotextdiff/span"
"github.com/jimeh/build-emacs-for-macos/pkg/gh"
"github.com/jimeh/build-emacs-for-macos/pkg/release"
"github.com/jimeh/build-emacs-for-macos/pkg/repository"
)
// Error vars
var (
Err = errors.New("cask")
ErrReleaseNotFound = fmt.Errorf("%w: release not found", Err)
ErrFailedSHA256Parse = fmt.Errorf(
"%w: failed to parse SHA256 from asset", Err,
)
ErrFailedSHA256Download = fmt.Errorf(
"%w: failed to download SHA256 asset", Err,
)
ErrNoTapOrOutput = fmt.Errorf(
"%w: no tap repository or output directory specified", Err,
)
)
type UpdateOptions struct {
// BuildsRepo is the GitHub repository containing binary releases.
BuildsRepo *repository.Repository
// TapRepo is the GitHub repository to update the casks in.
TapRepo *repository.Repository
// Ref is the git ref to apply cask updates on top of. Default branch will
// be used if empty.
Ref string
// OutputDir specifies a directory to write cask files to. When set, tap
// repository is ignored and no changes will be committed directly against
// any specified tap repository.
OutputDir string
// Force update will ignore the outdated live check flag, and process all
// casks regardless. But it will only update the cask in question if the
// resulting output cask is different.
Force bool
// TemplatesDir is the directory where cask templates are located.
TemplatesDir string
LiveChecks []*LiveCheck
GithubToken string
}
type Updater struct {
BuildsRepo *repository.Repository
TapRepo *repository.Repository
Ref string
OutputDir string
TemplatesDir string
logger hclog.Logger
gh *github.Client
}
func Update(ctx context.Context, opts *UpdateOptions) error {
updater := &Updater{
BuildsRepo: opts.BuildsRepo,
TapRepo: opts.TapRepo,
Ref: opts.Ref,
OutputDir: opts.OutputDir,
TemplatesDir: opts.TemplatesDir,
logger: hclog.FromContext(ctx).Named("cask"),
gh: gh.New(ctx, opts.GithubToken),
}
for _, chk := range opts.LiveChecks {
err := updater.Update(ctx, chk, opts.Force)
if err != nil {
return err
}
}
return nil
}
func (s *Updater) Update(
ctx context.Context,
chk *LiveCheck,
force bool,
) error {
if s.TapRepo == nil && s.OutputDir == "" {
return ErrNoTapOrOutput
}
if !force && !chk.Version.Outdated {
s.logger.Info("skipping", "cask", chk.Cask, "reason", "up to date")
return nil
}
newCaskContent, err := s.renderCask(ctx, chk)
if err != nil {
return err
}
caskFile := chk.Cask + ".rb"
if s.OutputDir != "" {
_, err = s.putFile(
ctx, chk, filepath.Join(s.OutputDir, caskFile), newCaskContent,
)
if err != nil {
return err
}
return nil
}
_, err = s.putRepoFile(
ctx, s.TapRepo, s.Ref, chk,
filepath.Join("Casks", caskFile), newCaskContent,
)
if err != nil {
return err
}
return nil
}
func (s *Updater) putFile(
ctx context.Context,
chk *LiveCheck,
filename string,
content []byte,
) (bool, error) {
parent := filepath.Dir(filename)
s.logger.Info("processing cask update",
"output-directory", parent, "cask", chk.Cask, "file", filename,
)
err := os.MkdirAll(parent, 0o755)
if err != nil {
return false, err
}
existingContent, err := os.ReadFile(filename)
if err != nil && !os.IsNotExist(err) {
return false, err
}
infoMsg := "creating cask"
if !os.IsNotExist(err) {
infoMsg = "updating cask"
if bytes.Equal(existingContent, content) {
s.logger.Info(
"skip update: no change to cask content",
"cask", chk.Cask, "file", filename,
)
s.logger.Debug(
"cask content",
"file", filename, "content", string(content),
)
return false, nil
}
}
existing := string(existingContent)
edits := myers.ComputeEdits(
span.URIFromPath(filename), existing, string(content),
)
diff := fmt.Sprint(gotextdiff.ToUnified(
filename, filename, existing, edits,
))
s.logger.Info(
infoMsg,
"cask", chk.Cask, "version", chk.Version.Latest, "file", filename,
"diff", diff,
)
s.logger.Debug(
"cask content",
"file", filename, "content", string(content),
)
err = os.WriteFile(filename, content, 0o644) //nolint:gosec
if err != nil {
return false, err
}
return true, nil
}
func (s *Updater) putRepoFile(
ctx context.Context,
repo *repository.Repository,
ref string,
chk *LiveCheck,
filename string,
content []byte,
) (bool, error) {
s.logger.Info("processing cask update",
"tap-repo", repo.Source, "cask", chk.Cask, "file", filename,
)
repoContent, _, resp, err := s.gh.Repositories.GetContents(
ctx, repo.Owner(), repo.Name(), filename,
&github.RepositoryContentGetOptions{Ref: ref},
)
if err != nil && resp.StatusCode != http.StatusNotFound {
return false, err
}
if resp.StatusCode == http.StatusNotFound {
err := s.createRepoFile(ctx, repo, chk, filename, content)
if err != nil {
return false, err
}
} else {
_, err := s.updateRepoFile(
ctx, repo, repoContent, chk, filename, content,
)
if err != nil {
return false, err
}
}
return true, nil
}
func (s *Updater) createRepoFile(
ctx context.Context,
repo *repository.Repository,
chk *LiveCheck,
filename string,
content []byte,
) error {
commitMsg := fmt.Sprintf(
"feat(cask): create %s with version %s",
chk.Cask, chk.Version.Latest,
)
edits := myers.ComputeEdits(
span.URIFromPath(filename), "", string(content),
)
diff := fmt.Sprint(gotextdiff.ToUnified(filename, filename, "", edits))
s.logger.Info(
"creating cask",
"cask", chk.Cask, "version", chk.Version.Latest, "file", filename,
"diff", diff,
)
s.logger.Debug(
"cask content",
"file", filename, "content", string(content),
)
contResp, _, err := s.gh.Repositories.CreateFile(
ctx, repo.Owner(), repo.Name(), filename,
&github.RepositoryContentFileOptions{
Message: &commitMsg,
Content: content,
},
)
if err != nil {
return err
}
s.logger.Info(
"new commit created",
"commit", contResp.GetSHA(), "message", contResp.GetMessage(),
"url", contResp.Commit.GetHTMLURL(),
)
return nil
}
func (s *Updater) updateRepoFile(
ctx context.Context,
repo *repository.Repository,
repoContent *github.RepositoryContent,
chk *LiveCheck,
filename string,
content []byte,
) (bool, error) {
existingContent, err := repoContent.GetContent()
if err != nil {
return false, err
}
if existingContent == string(content) {
s.logger.Info(
"skip update: no change to cask content",
"cask", chk.Cask, "file", filename,
)
return false, nil
}
sha := repoContent.GetSHA()
commitMsg := fmt.Sprintf(
"feat(cask): update %s to version %s",
chk.Cask, chk.Version.Latest,
)
edits := myers.ComputeEdits(
span.URIFromPath(filename), existingContent, string(content),
)
diff := fmt.Sprint(gotextdiff.ToUnified(
filename, filename, existingContent, edits,
))
s.logger.Info(
"updating cask",
"cask", chk.Cask, "version", chk.Version.Latest, "file", filename,
"diff", diff,
)
s.logger.Debug(
"cask content",
"file", filename, "content", string(content),
)
contResp, _, err := s.gh.Repositories.CreateFile(
ctx, repo.Owner(), repo.Name(), filename,
&github.RepositoryContentFileOptions{
Message: &commitMsg,
Content: content,
SHA: &sha,
},
)
if err != nil {
return false, err
}
s.logger.Info(
"new commit created",
"commit", contResp.GetSHA(), "message", contResp.GetMessage(),
"url", contResp.Commit.GetHTMLURL(),
)
return true, nil
}
func (s *Updater) renderCask(
ctx context.Context,
chk *LiveCheck,
) ([]byte, error) {
releaseName, err := release.VersionToName(chk.Version.Latest)
if err != nil {
return nil, err
}
s.logger.Info("fetching release details",
"release", releaseName, "repo", s.BuildsRepo.URL(),
)
release, resp, err := s.gh.Repositories.GetReleaseByTag(
ctx, s.BuildsRepo.Owner(), s.BuildsRepo.Name(), releaseName,
)
if err != nil {
return nil, err
}
if release == nil || resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("%w: %s", ErrReleaseNotFound, releaseName)
}
info := &ReleaseInfo{
Name: release.GetName(),
Version: chk.Version.Latest,
Assets: map[string]*ReleaseAsset{},
}
s.logger.Info("processing release assets")
for _, asset := range release.Assets {
filename := asset.GetName()
s.logger.Debug("processing asset", "filename", filename)
if strings.HasSuffix(filename, ".sha256") {
filename = strings.TrimSuffix(filename, ".sha256")
}
if _, ok := info.Assets[filename]; !ok {
info.Assets[filename] = &ReleaseAsset{
Filename: filename,
}
}
if strings.HasSuffix(asset.GetName(), ".sha256") {
s.logger.Debug("downloading *.sha256 asset to extract SHA256 value")
r, err2 := s.downloadAssetContent(ctx, asset)
if err2 != nil {
return nil, err2
}
defer r.Close()
content := make([]byte, 64)
n, err2 := io.ReadAtLeast(r, content, 64)
if err2 != nil {
return nil, err2
}
if n < 64 {
return nil, fmt.Errorf(
"%w: %s", ErrFailedSHA256Parse, asset.GetName(),
)
}
sha := string(content)[0:64]
if sha == "" {
return nil, fmt.Errorf(
"%w: %s", ErrFailedSHA256Parse, asset.GetName(),
)
}
info.Assets[filename].SHA256 = sha
} else {
info.Assets[filename].DownloadURL = asset.GetBrowserDownloadURL()
}
}
templateFile := filepath.Join(s.TemplatesDir, chk.Cask+".rb.tpl")
tplContent, err := os.ReadFile(templateFile)
if err != nil {
return nil, err
}
tpl, err := template.New(chk.Cask).Parse(string(tplContent))
if err != nil {
return nil, err
}
var buf bytes.Buffer
err = tpl.Execute(&buf, info)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (s *Updater) downloadAssetContent(
ctx context.Context,
asset *github.ReleaseAsset,
) (io.ReadCloser, error) {
httpClient := &http.Client{Timeout: 60 * time.Second}
r, downloadURL, err := s.gh.Repositories.DownloadReleaseAsset(
ctx, s.BuildsRepo.Owner(), s.BuildsRepo.Name(),
asset.GetID(), httpClient,
)
if err != nil {
return nil, err
}
if r == nil && downloadURL != "" {
req, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil)
if err != nil {
return nil, err
}
//nolint:bodyclose
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
r = resp.Body
}
if r == nil {
return nil, fmt.Errorf(
"%s: %s", ErrFailedSHA256Download, asset.GetName(),
)
}
return r, nil
}

158
pkg/cli/cask.go Normal file
View File

@@ -0,0 +1,158 @@
package cli
import (
"encoding/json"
"errors"
"os"
"github.com/jimeh/build-emacs-for-macos/pkg/cask"
"github.com/jimeh/build-emacs-for-macos/pkg/repository"
cli2 "github.com/urfave/cli/v2"
)
type caskOptions struct {
BuildsRepo *repository.Repository
GithubToken string
}
func caskCmd() *cli2.Command {
tokenDefaultText := ""
if len(os.Getenv("GITHUB_TOKEN")) > 0 {
tokenDefaultText = "***"
}
return &cli2.Command{
Name: "cask",
Usage: "manage Homebrew Casks",
Flags: []cli2.Flag{
&cli2.StringFlag{
Name: "builds-repository",
Aliases: []string{"builds-repo", "b"},
Usage: "owner/name of GitHub repo for containing builds",
EnvVars: []string{"EMACS_BUILDS_REPOSITORY"},
Value: "jimeh/emacs-builds",
},
&cli2.StringFlag{
Name: "github-token",
Usage: "GitHub API Token",
EnvVars: []string{"GITHUB_TOKEN"},
DefaultText: tokenDefaultText,
Required: true,
},
},
Subcommands: []*cli2.Command{
caskUpdateCmd(),
},
}
}
func caskActionWrapper(
f func(*cli2.Context, *Options, *caskOptions) error,
) func(*cli2.Context) error {
return actionWrapper(func(c *cli2.Context, opts *Options) error {
rOpts := &caskOptions{
GithubToken: c.String("github-token"),
}
if r := c.String("builds-repository"); r != "" {
var err error
rOpts.BuildsRepo, err = repository.NewGitHub(r)
if err != nil {
return err
}
}
return f(c, opts, rOpts)
})
}
func caskUpdateCmd() *cli2.Command {
return &cli2.Command{
Name: "update",
Usage: "update casks based on brew livecheck result in JSON format",
ArgsUsage: "<livecheck.json>",
Flags: []cli2.Flag{
&cli2.StringFlag{
Name: "ref",
Usage: "git ref to create/update casks on top of in the " +
"tap repository",
EnvVars: []string{"GITHUB_REF"},
},
&cli2.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "directory to write cask files to",
},
&cli2.StringFlag{
Name: "tap-repository",
Aliases: []string{"tap"},
Usage: "owner/name of GitHub repo for Homebrew Tap to " +
"commit changes to if --output is not set",
EnvVars: []string{"GITHUB_REPOSITORY"},
},
&cli2.StringFlag{
Name: "templates-dir",
Aliases: []string{"t"},
Usage: "path to directory of cask templates",
EnvVars: []string{"CASK_TEMPLATE_DIR"},
Required: true,
},
&cli2.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "force update file even if livecheck has it marked " +
"as not outdated (does not force update if cask " +
"content is unchanged)",
Value: false,
},
},
Action: caskActionWrapper(caskUpdateAction),
}
}
func caskUpdateAction(
c *cli2.Context,
opts *Options,
cOpts *caskOptions,
) error {
updateOpts := &cask.UpdateOptions{
BuildsRepo: cOpts.BuildsRepo,
GithubToken: cOpts.GithubToken,
Ref: c.String("ref"),
OutputDir: c.String("output"),
Force: c.Bool("force"),
TemplatesDir: c.String("templates-dir"),
}
if r := c.String("tap-repository"); r != "" {
var err error
updateOpts.TapRepo, err = repository.NewGitHub(r)
if err != nil {
return err
}
}
arg := c.Args().First()
if arg == "" {
return errors.New("no livecheck argument given")
}
if arg == "-" {
err := json.NewDecoder(c.App.Reader).Decode(&updateOpts.LiveChecks)
if err != nil {
return err
}
} else {
f, err := os.Open(arg)
if err != nil {
return err
}
err = json.NewDecoder(f).Decode(&updateOpts.LiveChecks)
if err != nil {
return err
}
}
return cask.Update(c.Context, updateOpts)
}

95
pkg/cli/cli.go Normal file
View File

@@ -0,0 +1,95 @@
package cli
import (
"fmt"
"strings"
cli2 "github.com/urfave/cli/v2"
)
type CLI struct {
App *cli2.App
Version string
Commit string
Date string
}
func New(version, commit, date string) *CLI {
if version == "" {
version = "0.0.0-dev"
}
c := &CLI{
Version: version,
Commit: commit,
Date: date,
App: &cli2.App{
Name: "emacs-builder",
Usage: "Tool to build emacs",
Version: version,
EnableBashCompletion: true,
Flags: []cli2.Flag{
&cli2.StringFlag{
Name: "log-level",
Usage: "set log level",
Aliases: []string{"l"},
Value: "info",
},
&cli2.BoolFlag{
Name: "quiet",
Usage: "silence noisy output",
Aliases: []string{"q"},
Value: false,
},
cli2.VersionFlag,
},
Commands: []*cli2.Command{
planCmd(),
signCmd(),
notarizeCmd(),
packageCmd(),
releaseCmd(),
caskCmd(),
{
Name: "version",
Usage: "print the version",
Aliases: []string{"v"},
Action: func(c *cli2.Context) error {
cli2.VersionPrinter(c)
return nil
},
},
},
},
}
cli2.VersionPrinter = c.VersionPrinter
return c
}
func (s *CLI) VersionPrinter(c *cli2.Context) {
version := c.App.Version
if version == "" {
version = "0.0.0-dev"
}
extra := []string{}
if len(s.Commit) >= 7 {
extra = append(extra, s.Commit[0:7])
}
if s.Date != "" {
extra = append(extra, s.Date)
}
var extraOut string
if len(extra) > 0 {
extraOut += " (" + strings.Join(extra, ", ") + ")"
}
fmt.Printf("%s%s\n", version, extraOut)
}
func (s *CLI) Run(args []string) error {
return s.App.Run(args)
}

79
pkg/cli/notarize.go Normal file
View File

@@ -0,0 +1,79 @@
package cli
import (
"path/filepath"
"github.com/jimeh/build-emacs-for-macos/pkg/notarize"
"github.com/jimeh/build-emacs-for-macos/pkg/plan"
cli2 "github.com/urfave/cli/v2"
)
func notarizeCmd() *cli2.Command {
return &cli2.Command{
Name: "notarize",
Usage: "notarize and staple a dmg, zip, or pkg",
ArgsUsage: "<file>",
Flags: []cli2.Flag{
&cli2.StringFlag{
Name: "bundle-id",
Usage: "bundle identifier",
Value: "org.gnu.Emacs",
},
&cli2.StringFlag{
Name: "ac-username",
Usage: "Apple Connect username",
EnvVars: []string{"AC_USERNAME"},
},
&cli2.StringFlag{
Name: "ac-password",
Usage: "Apple Connect password",
Value: "@env:AC_PASSWORD",
},
&cli2.StringFlag{
Name: "ac-provider",
Usage: "Apple Connect provider",
EnvVars: []string{"AC_PROVIDER"},
},
&cli2.BoolFlag{
Name: "staple",
Usage: "staple file after notarization",
Value: true,
},
&cli2.StringFlag{
Name: "plan",
Usage: "path to build plan YAML file produced by " +
"emacs-builder plan",
Aliases: []string{"p"},
EnvVars: []string{"EMACS_BUILDER_PLAN"},
TakesFile: true,
},
},
Action: actionWrapper(notarizeAction),
}
}
func notarizeAction(c *cli2.Context, opts *Options) error {
options := &notarize.Options{
File: c.Args().Get(0),
BundleID: c.String("bundle-id"),
Username: c.String("ac-username"),
Password: c.String("ac-password"),
Provider: c.String("ac-provider"),
Staple: c.Bool("staple"),
}
if f := c.String("plan"); f != "" {
p, err := plan.Load(f)
if err != nil {
return err
}
if p.Output != nil {
options.File = filepath.Join(
p.Output.Directory, p.Output.DiskImage,
)
}
}
return notarize.Notarize(c.Context, options)
}

49
pkg/cli/options.go Normal file
View File

@@ -0,0 +1,49 @@
package cli
import (
"fmt"
"os"
"sync"
"time"
"github.com/hashicorp/go-hclog"
cli2 "github.com/urfave/cli/v2"
)
type Options struct {
quiet bool
}
func actionWrapper(
f func(*cli2.Context, *Options) error,
) func(*cli2.Context) error {
return func(c *cli2.Context) error {
opts := &Options{
quiet: c.Bool("quiet"),
}
levelStr := c.String("log-level")
level := hclog.LevelFromString(levelStr)
if level == hclog.NoLevel {
return fmt.Errorf("invalid log level \"%s\"", levelStr)
}
// Prevent things from logging if they weren't explicitly given a
// logger.
hclog.SetDefault(hclog.NewNullLogger())
// Create custom logger.
logr := hclog.New(&hclog.LoggerOptions{
Level: level,
Output: os.Stderr,
Mutex: &sync.Mutex{},
TimeFormat: time.RFC3339,
Color: hclog.ColorOff,
})
ctx := hclog.WithContext(c.Context, logr)
c.Context = ctx
return f(c, opts)
}
}

230
pkg/cli/package.go Normal file
View File

@@ -0,0 +1,230 @@
package cli
import (
"crypto/sha256"
"fmt"
"io"
"os"
"path/filepath"
"github.com/hashicorp/go-hclog"
"github.com/jimeh/build-emacs-for-macos/pkg/dmg"
"github.com/jimeh/build-emacs-for-macos/pkg/notarize"
"github.com/jimeh/build-emacs-for-macos/pkg/plan"
"github.com/jimeh/build-emacs-for-macos/pkg/sign"
cli2 "github.com/urfave/cli/v2"
)
func packageCmd() *cli2.Command {
return &cli2.Command{
Name: "package",
Usage: "package a build directory containing Emacs.app into a dmg",
ArgsUsage: "<source-dir>",
Flags: []cli2.Flag{
&cli2.StringFlag{
Name: "volume-name",
Usage: "set volume name, defaults to basename of source dir",
Aliases: []string{"n"},
},
&cli2.BoolFlag{
Name: "sign",
Usage: "sign Emacs.app before packaging, notarize and staple " +
"dmg after packaging",
},
&cli2.StringFlag{
Name: "output",
Usage: "specify output dmg file name, if not specified the " +
"output filename is based on source directory",
Aliases: []string{"o"},
},
&cli2.BoolFlag{
Name: "sha256",
Usage: "create .sha256 checksum file for output dmg",
Aliases: []string{"s"},
Value: true,
},
&cli2.BoolFlag{
Name: "remove-source-dir",
Usage: "remove source directory after successfully " +
"creating dmg",
Aliases: []string{"rm"},
Value: false,
},
&cli2.BoolFlag{
Name: "verbose",
Usage: "verbose output",
Aliases: []string{"v"},
Value: false,
},
&cli2.StringFlag{
Name: "dmgbuild",
Usage: "specify custom path to dmgbuild executable",
},
&cli2.StringFlag{
Name: "sign-identity",
Usage: "(with --sign) signing identity passed to codesign",
EnvVars: []string{"AC_SIGN_IDENTITY"},
},
&cli2.StringFlag{
Name: "bundle-id",
Usage: "(with --sign) bundle identifier",
Value: "org.gnu.Emacs",
},
&cli2.StringFlag{
Name: "ac-username",
Usage: "(with --sign) Apple Connect username",
EnvVars: []string{"AC_USERNAME"},
},
&cli2.StringFlag{
Name: "ac-password",
Usage: "(with --sign) Apple Connect password",
Value: "@env:AC_PASSWORD",
},
&cli2.StringFlag{
Name: "ac-provider",
Usage: "(with --sign) Apple Connect provider",
EnvVars: []string{"AC_PROVIDER"},
},
&cli2.BoolFlag{
Name: "staple",
Usage: "(with --sign) stable after notarization",
Value: true,
},
&cli2.StringFlag{
Name: "plan",
Usage: "path to build plan YAML file produced by " +
"emacs-builder plan",
Aliases: []string{"p"},
EnvVars: []string{"EMACS_BUILDER_PLAN"},
TakesFile: true,
},
},
Action: actionWrapper(packageAction),
}
}
//nolint:funlen
func packageAction(c *cli2.Context, opts *Options) error {
logger := hclog.FromContext(c.Context).Named("package")
sourceDir := c.Args().Get(0)
doSign := c.Bool("sign")
var p *plan.Plan
var err error
if f := c.String("plan"); f != "" {
p, err = plan.Load(f)
if err != nil {
return err
}
}
if doSign {
app := filepath.Join(sourceDir, "Emacs.app")
signOpts := &sign.Options{
Identity: c.String("sign-identity"),
Options: []string{"runtime"},
Deep: true,
Timestamp: true,
Force: true,
Verbose: c.Bool("verbose"),
}
if p != nil {
if p.Output != nil && p.Build != nil {
app = filepath.Join(
p.Output.Directory, p.Build.Name, "Emacs.app",
)
}
}
if !opts.quiet {
signOpts.Output = os.Stdout
}
err = sign.Emacs(c.Context, app, signOpts)
if err != nil {
return err
}
}
dmgOpts := &dmg.Options{
DMGBuild: c.String("dmgbuild"),
SourceDir: sourceDir,
VolumeName: c.String("volume-name"),
OutputFile: c.String("output"),
RemoveSourceDir: c.Bool("remove-source-dir"),
Verbose: c.Bool("verbose"),
}
if p != nil && p.Output != nil && p.Build != nil {
dmgOpts.SourceDir = filepath.Join(
p.Output.Directory, p.Build.Name,
)
dmgOpts.VolumeName = p.Build.Name
dmgOpts.OutputFile = filepath.Join(
p.Output.Directory, p.Output.DiskImage,
)
}
if !opts.quiet {
dmgOpts.Output = os.Stdout
}
outputDMG, err := dmg.Create(c.Context, dmgOpts)
if err != nil {
return err
}
if doSign {
notarizeOpts := &notarize.Options{
File: outputDMG,
BundleID: c.String("bundle-id"),
Username: c.String("ac-username"),
Password: c.String("ac-password"),
Provider: c.String("ac-provider"),
Staple: c.Bool("staple"),
}
err = notarize.Notarize(c.Context, notarizeOpts)
if err != nil {
return err
}
}
if c.Bool("sha256") {
sumFile := outputDMG + ".sha256"
logger.Info("generating SHA256 checksum", "file", outputDMG)
sum, err := fileSHA256(outputDMG)
if err != nil {
return err
}
logger.Info("checksum", "sha256", sum, "file", outputDMG)
content := fmt.Sprintf("%s %s", sum, filepath.Base(outputDMG))
err = os.WriteFile(sumFile, []byte(content), 0o644) //nolint:gosec
if err != nil {
return err
}
logger.Info("wrote checksum", "file", sumFile)
}
return nil
}
func fileSHA256(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}

128
pkg/cli/plan.go Normal file
View File

@@ -0,0 +1,128 @@
package cli
import (
"os"
"path/filepath"
"github.com/hashicorp/go-hclog"
"github.com/jimeh/build-emacs-for-macos/pkg/plan"
cli2 "github.com/urfave/cli/v2"
)
func planCmd() *cli2.Command {
wd, err := os.Getwd()
if err != nil {
wd = ""
}
tokenDefaultText := ""
if len(os.Getenv("GITHUB_TOKEN")) > 0 {
tokenDefaultText = "***"
}
return &cli2.Command{
Name: "plan",
Usage: "plan a Emacs.app bundle with codeplan",
ArgsUsage: "<branch/tag>",
Flags: []cli2.Flag{
&cli2.StringFlag{
Name: "emacs-repo",
Usage: "GitHub repository to get Emacs commit info and " +
"tarball from",
Aliases: []string{"e"},
EnvVars: []string{"EMACS_REPO"},
Value: "emacs-mirror/emacs",
},
&cli2.StringFlag{
Name: "sha",
Usage: "override commit SHA of specified git branch/tag",
},
&cli2.StringFlag{
Name: "output",
Usage: "output filename to write plan to instead of printing " +
"to STDOUT",
Aliases: []string{"o"},
},
&cli2.StringFlag{
Name: "output-dir",
Usage: "output directory where build result is stored",
Value: filepath.Join(wd, "builds"),
},
&cli2.StringFlag{
Name: "test-build",
Usage: "plan a test build with given name, which is " +
"published to a draft or pre-release " +
"\"test-builds\" release",
},
&cli2.StringFlag{
Name: "test-release-type",
Value: "prerelease",
Usage: "type of release when doing a test-build " +
"(prerelease or draft)",
},
&cli2.StringFlag{
Name: "github-token",
Usage: "GitHub API Token",
EnvVars: []string{"GITHUB_TOKEN"},
DefaultText: tokenDefaultText,
},
},
Action: actionWrapper(planAction),
}
}
func planAction(c *cli2.Context, opts *Options) error {
logger := hclog.FromContext(c.Context).Named("plan")
ref := c.Args().Get(0)
if ref == "" {
ref = "master"
}
planOpts := &plan.Options{
EmacsRepo: c.String("emacs-repo"),
Ref: ref,
SHAOverride: c.String("sha"),
OutputDir: c.String("output-dir"),
TestBuild: c.String("test-build"),
TestBuildType: plan.Prerelease,
GithubToken: c.String("github-token"),
}
if c.String("test-release-type") == "draft" {
planOpts.TestBuildType = plan.Draft
}
if !opts.quiet {
planOpts.Output = os.Stdout
}
p, err := plan.Create(c.Context, planOpts)
if err != nil {
return err
}
planYAML, err := p.YAML()
if err != nil {
return err
}
var out *os.File
out = os.Stdout
if f := c.String("output"); f != "" {
logger.Info("writing plan", "file", f)
logger.Debug("content", "yaml", planYAML)
out, err = os.Create(f)
if err != nil {
return err
}
defer out.Close()
}
_, err = out.WriteString(planYAML)
if err != nil {
return err
}
return nil
}

263
pkg/cli/release.go Normal file
View File

@@ -0,0 +1,263 @@
package cli
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/jimeh/build-emacs-for-macos/pkg/plan"
"github.com/jimeh/build-emacs-for-macos/pkg/release"
"github.com/jimeh/build-emacs-for-macos/pkg/repository"
cli2 "github.com/urfave/cli/v2"
)
type releaseOptions struct {
Plan *plan.Plan
Repository *repository.Repository
Name string
GithubToken string
}
func releaseCmd() *cli2.Command {
tokenDefaultText := ""
if len(os.Getenv("GITHUB_TOKEN")) > 0 {
tokenDefaultText = "***"
}
return &cli2.Command{
Name: "release",
Usage: "manage GitHub releases",
Flags: []cli2.Flag{
&cli2.StringFlag{
Name: "plan",
Usage: "path to build plan YAML file produced by " +
"emacs-builder plan",
Aliases: []string{"p"},
EnvVars: []string{"EMACS_BUILDER_PLAN"},
TakesFile: true,
},
&cli2.StringFlag{
Name: "repository",
Aliases: []string{"repo", "r"},
Usage: "owner/name of GitHub repo to check for release, " +
"ignored if a plan is provided",
EnvVars: []string{"GITHUB_REPOSITORY"},
Value: "jimeh/emacs-builds",
},
&cli2.StringFlag{
Name: "name",
Aliases: []string{"n"},
Usage: "name of release to operate on, ignored if plan " +
"is provided",
},
&cli2.StringFlag{
Name: "github-token",
Usage: "GitHub API Token",
EnvVars: []string{"GITHUB_TOKEN"},
DefaultText: tokenDefaultText,
},
},
Subcommands: []*cli2.Command{
releaseCheckCmd(),
releasePublishCmd(),
releaseBulkCmd(),
},
}
}
func releaseActionWrapper(
f func(*cli2.Context, *Options, *releaseOptions) error,
) func(*cli2.Context) error {
return actionWrapper(func(c *cli2.Context, opts *Options) error {
rOpts := &releaseOptions{
Name: c.String("name"),
GithubToken: c.String("github-token"),
}
if r := c.String("repository"); r != "" {
var err error
rOpts.Repository, err = repository.NewGitHub(r)
if err != nil {
return err
}
}
if f := c.String("plan"); f != "" {
p, err := plan.Load(f)
if err != nil {
return err
}
rOpts.Plan = p
}
return f(c, opts, rOpts)
})
}
func releaseCheckCmd() *cli2.Command {
return &cli2.Command{
Name: "check",
Usage: "check if a GitHub release exists and has specified " +
"asset files",
ArgsUsage: "[<asset-file> ...]",
Action: releaseActionWrapper(releaseCheckAction),
}
}
func releaseCheckAction(
c *cli2.Context,
opts *Options,
rOpts *releaseOptions,
) error {
rlsOpts := &release.CheckOptions{
Repository: rOpts.Repository,
ReleaseName: rOpts.Name,
AssetFiles: c.Args().Slice(),
GithubToken: rOpts.GithubToken,
}
if rOpts.Plan != nil && rOpts.Plan.Release != nil {
rlsOpts.ReleaseName = rOpts.Plan.Release.Name
}
if rOpts.Plan != nil && rOpts.Plan.Output != nil {
rlsOpts.AssetFiles = []string{rOpts.Plan.Output.DiskImage}
}
return release.Check(c.Context, rlsOpts)
}
func releasePublishCmd() *cli2.Command {
return &cli2.Command{
Name: "publish",
Usage: "publish a GitHub release with specified asset " +
"files",
ArgsUsage: "[<asset-file> ...]",
Flags: []cli2.Flag{
&cli2.StringFlag{
Name: "sha",
Aliases: []string{"s"},
Usage: "git SHA to create release on",
EnvVars: []string{"GITHUB_SHA"},
},
&cli2.StringFlag{
Name: "type",
Aliases: []string{"t"},
Usage: "release type, must be normal, prerelease, or draft",
Value: "normal",
},
&cli2.StringFlag{
Name: "title",
Usage: "release title, will use release name if not " +
"specified",
Value: "",
},
},
Action: releaseActionWrapper(releasePublishAction),
}
}
func releasePublishAction(
c *cli2.Context,
opts *Options,
rOpts *releaseOptions,
) error {
rlsOpts := &release.PublishOptions{
Repository: rOpts.Repository,
CommitRef: c.String("release-sha"),
ReleaseName: rOpts.Name,
ReleaseTitle: c.String("title"),
AssetFiles: c.Args().Slice(),
GithubToken: rOpts.GithubToken,
}
rlsType := c.String("type")
switch rlsType {
case "draft":
rlsOpts.ReleaseType = release.Draft
case "prerelease":
rlsOpts.ReleaseType = release.Prerelease
case "normal":
rlsOpts.ReleaseType = release.Normal
default:
return fmt.Errorf("invalid --type \"%s\"", rlsType)
}
if rOpts.Plan != nil {
if rOpts.Plan.Release != nil {
rlsOpts.ReleaseName = rOpts.Plan.Release.Name
rlsOpts.ReleaseTitle = rOpts.Plan.Release.Title
if rOpts.Plan.Release.Draft {
rlsOpts.ReleaseType = release.Draft
} else if rOpts.Plan.Release.Prerelease {
rlsOpts.ReleaseType = release.Prerelease
}
}
if rOpts.Plan.Output != nil {
rlsOpts.AssetFiles = []string{
filepath.Join(
rOpts.Plan.Output.Directory,
rOpts.Plan.Output.DiskImage,
),
}
}
}
return release.Publish(c.Context, rlsOpts)
}
func releaseBulkCmd() *cli2.Command {
return &cli2.Command{
Name: "bulk",
Usage: "bulk modify GitHub releases",
ArgsUsage: "",
Flags: []cli2.Flag{
&cli2.StringFlag{
Name: "name",
Usage: "regexp pattern matching release names to modify",
},
&cli2.StringFlag{
Name: "prerelease",
Usage: "change prerelease flag, must be \"true\" or " +
"\"false\", otherwise prerelease value is not changed",
},
&cli2.BoolFlag{
Name: "dry-run",
Usage: "do not perform any changes",
},
},
Action: releaseActionWrapper(releaseBulkAction),
}
}
func releaseBulkAction(
c *cli2.Context,
opts *Options,
rOpts *releaseOptions,
) error {
bulkOpts := &release.BulkOptions{
Repository: rOpts.Repository,
NamePattern: c.String("name"),
DryRun: c.Bool("dry-run"),
GithubToken: rOpts.GithubToken,
}
switch c.String("prerelease") {
case "true":
v := true
bulkOpts.Prerelease = &v
case "false":
v := false
bulkOpts.Prerelease = &v
case "":
default:
return errors.New(
"--prerelease by me \"true\" or \"false\" when specified",
)
}
return release.Bulk(c.Context, bulkOpts)
}

114
pkg/cli/sign.go Normal file
View File

@@ -0,0 +1,114 @@
package cli
import (
"os"
"path/filepath"
"github.com/jimeh/build-emacs-for-macos/pkg/plan"
"github.com/jimeh/build-emacs-for-macos/pkg/sign"
cli2 "github.com/urfave/cli/v2"
)
func signCmd() *cli2.Command {
return &cli2.Command{
Name: "sign",
Usage: "sign a Emacs.app bundle with codesign",
ArgsUsage: "<emacs-app>",
Flags: []cli2.Flag{
&cli2.StringFlag{
Name: "sign",
Aliases: []string{"s"},
Usage: "signing identity passed to codesign",
EnvVars: []string{"AC_SIGN_IDENTITY"},
Required: true,
},
&cli2.StringSliceFlag{
Name: "entitlements",
Aliases: []string{"e"},
Usage: "comma-separated list of entitlements to enable",
Value: cli2.NewStringSlice(sign.DefaultEmacsEntitlements...),
},
&cli2.BoolFlag{
Name: "deep",
Aliases: []string{"d"},
Usage: "pass --deep to codesign",
Value: true,
},
&cli2.BoolFlag{
Name: "timestamp",
Aliases: []string{"t"},
Usage: "pass --timestamp to codesign",
Value: true,
},
&cli2.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "pass --force to codesign",
Value: true,
},
&cli2.BoolFlag{
Name: "verbose",
Aliases: []string{"v"},
Usage: "pass --verbose to codesign",
Value: false,
},
&cli2.StringSliceFlag{
Name: "options",
Aliases: []string{"o"},
Usage: "options passed to codesign",
Value: cli2.NewStringSlice("runtime"),
},
&cli2.StringFlag{
Name: "codesign",
Usage: "specify custom path to codesign executable",
},
&cli2.StringFlag{
Name: "plan",
Usage: "path to build plan YAML file produced by " +
"emacs-builder plan",
Aliases: []string{"p"},
EnvVars: []string{"EMACS_BUILDER_PLAN"},
TakesFile: true,
},
},
Action: actionWrapper(signAction),
}
}
func signAction(c *cli2.Context, opts *Options) error {
signOpts := &sign.Options{
Identity: c.String("sign"),
Options: c.StringSlice("options"),
Deep: c.Bool("deep"),
Timestamp: c.Bool("timestamp"),
Force: c.Bool("force"),
Verbose: c.Bool("verbose"),
CodeSignCmd: c.String("codesign"),
}
if v := c.StringSlice("entitlements"); len(v) > 0 {
e := sign.Entitlements(v)
signOpts.Entitlements = &e
}
if !opts.quiet {
signOpts.Output = os.Stdout
}
app := c.Args().Get(0)
if f := c.String("plan"); f != "" {
p, err := plan.Load(f)
if err != nil {
return err
}
if p.Output != nil && p.Build != nil {
app = filepath.Join(
p.Output.Directory, p.Build.Name, "Emacs.app",
)
}
}
return sign.Emacs(c.Context, app, signOpts)
}

42
pkg/commit/commit.go Normal file
View File

@@ -0,0 +1,42 @@
package commit
import (
"fmt"
"time"
"github.com/google/go-github/v35/github"
)
type Commit struct {
SHA string `yaml:"sha"`
Date *time.Time `yaml:"date"`
Author string `yaml:"author"`
Committer string `yaml:"committer"`
Message string `yaml:"message"`
}
func New(rc *github.RepositoryCommit) *Commit {
return &Commit{
SHA: rc.GetSHA(),
Date: rc.GetCommit().GetCommitter().Date,
Author: fmt.Sprintf(
"%s <%s>",
rc.GetCommit().GetAuthor().GetName(),
rc.GetCommit().GetAuthor().GetEmail(),
),
Committer: fmt.Sprintf(
"%s <%s>",
rc.GetCommit().GetCommitter().GetName(),
rc.GetCommit().GetCommitter().GetEmail(),
),
Message: rc.GetCommit().GetMessage(),
}
}
func (s *Commit) ShortSHA() string {
return s.SHA[0:7]
}
func (s *Commit) DateString() string {
return s.Date.Format("2006-01-02")
}

45
pkg/dmg/assets/assets.go Normal file
View File

@@ -0,0 +1,45 @@
package assets
import (
_ "embed"
"os"
)
//go:generate tiffutil -cathidpicheck bg.png bg@2x.png -out bg.tif
// Background is a raw byte slice of bytes of bg.tiff
//go:embed bg.tif
var Background []byte
// BackgroundTempFile writes Background to a temporary file on disk, returning
// the resulting file path. The returned filepath should be deleted with
// os.Remove() when no longer needed.
func BackgroundTempFile() (string, error) {
return tempFile("*-emacs-bg.tif", Background)
}
// Icon is a raw byte slice of bytes of vol.icns
//go:embed vol.icns
var Icon []byte
// IconTempFile writes Icon to a temporary file on disk, returning the resulting
// file path. The returned filepath should be deleted with os.Remove() when no
// longer needed.
func IconTempFile() (string, error) {
return tempFile("*-emacs-vol.icns", Icon)
}
func tempFile(pattern string, content []byte) (string, error) {
f, err := os.CreateTemp("", pattern)
if err != nil {
return "", err
}
defer f.Close()
_, err = f.Write(content)
if err != nil {
return "", err
}
return f.Name(), nil
}

BIN
pkg/dmg/assets/bg.afdesign Normal file

Binary file not shown.

BIN
pkg/dmg/assets/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
pkg/dmg/assets/bg.tif Normal file

Binary file not shown.

BIN
pkg/dmg/assets/bg@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
pkg/dmg/assets/vol.icns Normal file

Binary file not shown.

138
pkg/dmg/dmg.go Normal file
View File

@@ -0,0 +1,138 @@
package dmg
import (
"context"
"io"
"os"
"path/filepath"
"github.com/hashicorp/go-hclog"
"github.com/jimeh/build-emacs-for-macos/pkg/dmg/assets"
"github.com/jimeh/build-emacs-for-macos/pkg/dmgbuild"
)
type Options struct {
DMGBuild string
SourceDir string
VolumeName string
OutputFile string
RemoveSourceDir bool
Verbose bool
Output io.Writer
}
//nolint:funlen
// Create will create a *.dmg disk image as specified by the given Options.
func Create(ctx context.Context, opts *Options) (string, error) {
logger := hclog.FromContext(ctx).Named("package")
sourceDir, err := filepath.Abs(opts.SourceDir)
if err != nil {
return "", err
}
appBundle := filepath.Join(sourceDir, "Emacs.app")
_, err = os.Stat(appBundle)
if err != nil {
return "", err
}
volIcon, err := assets.IconTempFile()
if err != nil {
return "", err
}
defer os.Remove(volIcon)
bgImg, err := assets.BackgroundTempFile()
if err != nil {
return "", err
}
defer os.Remove(bgImg)
volName := opts.VolumeName
if volName == "" {
volName = filepath.Base(sourceDir)
}
outputDMG := opts.OutputFile
if outputDMG == "" {
outputDMG = sourceDir + ".dmg"
}
settings := &dmgbuild.Settings{
Logger: logger,
Filename: outputDMG,
VolumeName: volName,
Icon: volIcon,
Format: dmgbuild.UDZOFormat,
CompressionLevel: 9,
Files: []*dmgbuild.File{
{
Path: appBundle,
PosX: 170,
PosY: 200,
},
},
Symlinks: []*dmgbuild.Symlink{
{
Name: "Applications",
Target: "/Applications",
PosX: 510,
PosY: 200,
},
},
Window: dmgbuild.Window{
Background: bgImg,
PoxX: 200,
PosY: 200,
Width: 680,
Height: 446,
DefaultView: dmgbuild.Icon,
},
IconView: dmgbuild.IconView{
IconSize: 160,
TextSize: 16,
},
}
copyingFile := filepath.Join(sourceDir, "COPYING")
fi, err := os.Stat(copyingFile)
if err != nil && !os.IsNotExist(err) {
return "", err
} else if err == nil && fi.Mode().IsRegular() {
settings.Files = append(settings.Files, &dmgbuild.File{
Path: copyingFile,
PosX: 340,
PosY: 506,
})
}
if opts.Output != nil {
settings.Stdout = opts.Output
settings.Stderr = opts.Output
}
logger.Info("creating dmg", "file", filepath.Base(outputDMG))
err = dmgbuild.Build(ctx, settings)
if err != nil {
return "", err
}
if opts.RemoveSourceDir {
dir, err := filepath.Abs(opts.SourceDir)
if err != nil {
return "", err
}
logger.Info("removing", "source-dir", dir)
err = os.RemoveAll(dir)
if err != nil {
return "", err
}
}
return outputDMG, nil
}

84
pkg/dmgbuild/dmgbuild.go Normal file
View File

@@ -0,0 +1,84 @@
package dmgbuild
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"github.com/hashicorp/go-hclog"
)
func Build(ctx context.Context, settings *Settings) error {
if settings == nil {
return fmt.Errorf("no settings provided")
}
logger := hclog.NewNullLogger()
if settings.Logger != nil {
logger = settings.Logger
}
if !strings.HasSuffix(logger.Name(), "dmgbuild") {
logger = logger.Named("dmgbuild")
}
_, err := os.Stat(settings.Filename)
if !os.IsNotExist(err) {
return fmt.Errorf("output dmg exists: %s", settings.Filename)
}
baseCmd := settings.Command
if baseCmd == "" {
path, err2 := exec.LookPath("dmgbuild")
if err2 != nil {
return err2
}
baseCmd = path
}
file, err := settings.TempFile()
if err != nil {
return err
}
defer os.Remove(file)
args := []string{"-s", file, settings.VolumeName, settings.Filename}
if logger.IsDebug() {
content, err2 := os.ReadFile(file)
if err2 != nil {
return err2
}
logger.Debug("using settings", file, string(content))
logger.Debug("executing", "command", baseCmd, "args", args)
}
cmd := exec.CommandContext(ctx, baseCmd, args...)
if settings.Stdout != nil {
cmd.Stdout = settings.Stdout
}
if settings.Stderr != nil {
cmd.Stderr = settings.Stderr
}
err = cmd.Run()
if err != nil {
return err
}
f, err := os.Stat(settings.Filename)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("output DMG file is missing")
}
return err
}
if !f.Mode().IsRegular() {
return fmt.Errorf("output DMG file is not a file")
}
return nil
}

85
pkg/dmgbuild/icon_view.go Normal file
View File

@@ -0,0 +1,85 @@
package dmgbuild
import "fmt"
type arrageOrder string
//nolint:golint
var (
NameOrder arrageOrder = "name"
DateModifiedOrder arrageOrder = "date-modified"
DateCreatedOrder arrageOrder = "date-created"
DateAddedOrder arrageOrder = "date-added"
DateLastOpenedOrder arrageOrder = "date-last-opened"
SizeOrder arrageOrder = "size"
KindOrder arrageOrder = "kind"
LabelOrder arrageOrder = "label"
)
type labelPosition string
//nolint:golint
var (
LabelBottom labelPosition = "bottom"
LabelRight labelPosition = "right"
)
type IconView struct {
ArrangeBy arrageOrder
GridOffsetX int
GridOffsetY int
GridSpacing float32
ScrollPosX float32
ScrollPosY float32
LabelPosition labelPosition
IconSize float32
TextSize float32
}
func NewIconView() IconView {
return IconView{
GridOffsetX: 0,
GridOffsetY: 0,
GridSpacing: 100,
ScrollPosX: 0.0,
ScrollPosY: 0.0,
LabelPosition: LabelBottom,
IconSize: 128,
TextSize: 16,
}
}
func (s *IconView) Render() []string {
r := []string{}
if s.ArrangeBy != "" {
r = append(r, "arrange_by = "+pyStr(string(s.ArrangeBy))+"\n")
}
if s.GridOffsetX > 0 || s.GridOffsetY > 0 {
r = append(r, fmt.Sprintf(
"grid_offset = (%d, %d)\n",
s.GridOffsetX, s.GridOffsetY,
))
}
if s.GridSpacing > 0 {
r = append(r, fmt.Sprintf("grid_spacing = %.2f\n", s.GridSpacing))
}
if s.ScrollPosX > 0 || s.ScrollPosY > 0 {
r = append(r, fmt.Sprintf(
"scroll_position = (%.2f, %.2f)\n",
s.ScrollPosX, s.ScrollPosY,
))
}
if s.LabelPosition != "" {
r = append(r, "label_position = "+pyStr(string(s.LabelPosition))+"\n")
}
if s.IconSize > 0 {
r = append(r, fmt.Sprintf("icon_size = %.2f\n", s.IconSize))
}
if s.TextSize > 0 {
r = append(r, fmt.Sprintf("text_size = %.2f\n", s.TextSize))
}
return r
}

183
pkg/dmgbuild/license.go Normal file
View File

@@ -0,0 +1,183 @@
package dmgbuild
import (
"fmt"
"sort"
"strings"
)
type locale string
//nolint:golint
var (
LocaleAfZA locale = "af_ZA"
LocaleAr locale = "ar"
LocaleBeBY locale = "be_BY"
LocaleBgBG locale = "bg_BG"
LocaleBn locale = "bn"
LocaleBo locale = "bo"
LocaleBr locale = "br"
LocaleCaES locale = "ca_ES"
LocaleCsCZ locale = "cs_CZ"
LocaleCy locale = "cy"
LocaleDaDK locale = "da_DK"
LocaleDeAT locale = "de_AT"
LocaleDeCH locale = "de_CH"
LocaleDeDE locale = "de_DE"
LocaleDzBT locale = "dz_BT"
LocaleElCY locale = "el_CY"
LocaleElGR locale = "el_GR"
LocaleEnAU locale = "en_AU"
LocaleEnCA locale = "en_CA"
LocaleEnGB locale = "en_GB"
LocaleEnIE locale = "en_IE"
LocaleEnSG locale = "en_SG"
LocaleEnUS locale = "en_US"
LocaleEo locale = "eo"
LocaleEs419 locale = "es_419"
LocaleEsES locale = "es_ES"
LocaleEtEE locale = "et_EE"
LocaleFaIR locale = "fa_IR"
LocaleFiFI locale = "fi_FI"
LocaleFoFO locale = "fo_FO"
LocaleFr001 locale = "fr_001"
LocaleFrBE locale = "fr_BE"
LocaleFrCA locale = "fr_CA"
LocaleFrCH locale = "fr_CH"
LocaleFrFR locale = "fr_FR"
LocaleGaLatgIE locale = "ga-Latg_IE"
LocaleGaIE locale = "ga_IE"
LocaleGd locale = "gd"
LocaleGrc locale = "grc"
LocaleGuIN locale = "gu_IN"
LocaleGv locale = "gv"
LocaleHeIL locale = "he_IL"
LocaleHiIN locale = "hi_IN"
LocaleHrHR locale = "hr_HR"
LocaleHuHU locale = "hu_HU"
LocaleHyAM locale = "hy_AM"
LocaleIsIS locale = "is_IS"
LocaleItCH locale = "it_CH"
LocaleItIT locale = "it_IT"
LocaleIuCA locale = "iu_CA"
LocaleJaJP locale = "ja_JP"
LocaleKaGE locale = "ka_GE"
LocaleKl locale = "kl"
LocaleKoKR locale = "ko_KR"
LocaleLtLT locale = "lt_LT"
LocaleLvLV locale = "lv_LV"
LocaleMkMK locale = "mk_MK"
LocaleMrIN locale = "mr_IN"
LocaleMtMT locale = "mt_MT"
LocaleNbNO locale = "nb_NO"
LocaleNeNP locale = "ne_NP"
LocaleNlBE locale = "nl_BE"
LocaleNlNL locale = "nl_NL"
LocaleNnNO locale = "nn_NO"
LocalePa locale = "pa"
LocalePlPL locale = "pl_PL"
LocalePtBR locale = "pt_BR"
LocalePtPT locale = "pt_PT"
LocaleRoRO locale = "ro_RO"
LocaleRuRU locale = "ru_RU"
LocaleSe locale = "se"
LocaleSkSK locale = "sk_SK"
LocaleSlSI locale = "sl_SI"
LocaleSrRS locale = "sr_RS"
LocaleSvSE locale = "sv_SE"
LocaleThTH locale = "th_TH"
LocaleToTO locale = "to_TO"
LocaleTrTR locale = "tr_TR"
LocaleUkUA locale = "uk_UA"
LocaleUrIN locale = "ur_IN"
LocaleUrPK locale = "ur_PK"
LocaleUzUZ locale = "uz_UZ"
LocaleViVN locale = "vi_VN"
LocaleZhCN locale = "zh_CN"
LocaleZhTW locale = "zh_TW"
)
type Buttons struct {
LanguageName string
Agree string
Disagree string
Print string
Save string
Message string
}
type License struct {
DefaultLanguage locale
Licenses map[locale]string
Buttons map[locale]Buttons
}
func NewLicense() License {
return License{}
}
func (s *License) Render() []string {
var l []string
if s.DefaultLanguage != "" {
l = append(l,
"\"default-language\": "+pyStr(string(s.DefaultLanguage)),
)
}
if len(s.Licenses) > 0 {
var items []string
for k, v := range s.Licenses {
items = append(items, fmt.Sprintf(
"%s: %s", pyStr(string(k)), pyMStr(v),
))
}
sort.SliceStable(items, func(i, j int) bool {
return items[i] < items[j]
})
l = append(l,
"\"licenses\": {\n "+
strings.Join(items, ",\n ")+
"\n }",
)
}
if len(s.Buttons) > 0 {
var items []string
for k, v := range s.Buttons {
items = append(items, fmt.Sprintf(
"%s: (\n"+
" %s,\n"+
" %s,\n"+
" %s,\n"+
" %s,\n"+
" %s,\n"+
" %s\n"+
" )",
pyStr(string(k)),
pyStr(v.LanguageName),
pyStr(v.Agree),
pyStr(v.Disagree),
pyStr(v.Print),
pyStr(v.Save),
pyStr(v.Message),
))
}
sort.SliceStable(items, func(i, j int) bool {
return items[i] < items[j]
})
l = append(l,
"\"buttons\": {\n "+
strings.Join(items, ",\n ")+
"\n }",
)
}
if len(l) == 0 {
return []string{}
}
return []string{
"license = {\n " + strings.Join(l, ",\n ") + "\n}\n",
}
}

154
pkg/dmgbuild/list_view.go Normal file
View File

@@ -0,0 +1,154 @@
package dmgbuild
import (
"fmt"
"sort"
"strings"
)
type listColumn string
//nolint:golint
var (
NameColumn listColumn = "name"
DateModifiedColumn listColumn = "date-modified"
DateCreatedColumn listColumn = "date-created"
DateAddedColumn listColumn = "date-added"
DateLastOpenedColumn listColumn = "date-last-opened"
SizeColumn listColumn = "size"
KindColumn listColumn = "kind"
LabelColumn listColumn = "label"
VersionColumn listColumn = "version"
CommentsColumn listColumn = "comments"
)
type direction string
//nolint:golint
var (
Ascending direction = "ascending"
Descending direction = "descending"
)
type ListView struct {
SortBy listColumn
ScrollPosX int
ScrollPosY int
IconSize float32
TextSize float32
UseRelativeDates bool
CalculateAllSizes bool
Columns []listColumn
ColumnWidths map[listColumn]int
ColumnSortDirections map[listColumn]direction
}
func NewListView() ListView {
return ListView{
SortBy: NameColumn,
IconSize: 16,
TextSize: 12,
UseRelativeDates: true,
Columns: []listColumn{
NameColumn,
DateModifiedColumn,
SizeColumn,
KindColumn,
DateAddedColumn,
},
ColumnWidths: map[listColumn]int{
(NameColumn): 300,
(DateModifiedColumn): 181,
(DateCreatedColumn): 181,
(DateAddedColumn): 181,
(DateLastOpenedColumn): 181,
(SizeColumn): 97,
(KindColumn): 115,
(LabelColumn): 100,
(VersionColumn): 75,
(CommentsColumn): 300,
},
ColumnSortDirections: map[listColumn]direction{
(NameColumn): Ascending,
(DateModifiedColumn): Descending,
(DateCreatedColumn): Descending,
(DateAddedColumn): Descending,
(DateLastOpenedColumn): Descending,
(SizeColumn): Descending,
(KindColumn): Ascending,
(LabelColumn): Ascending,
(VersionColumn): Ascending,
(CommentsColumn): Ascending,
},
}
}
func (s *ListView) Render() []string {
r := []string{}
if s.SortBy != "" {
r = append(r, "list_sort_by = "+pyStr(string(s.SortBy))+"\n")
}
if s.ScrollPosX > 0 || s.ScrollPosY > 0 {
r = append(r, fmt.Sprintf(
"list_scroll_position = (%d, %d)\n",
s.ScrollPosX, s.ScrollPosY,
))
}
if s.IconSize > 0 {
r = append(r, fmt.Sprintf("list_icon_size = %.2f\n", s.IconSize))
}
if s.TextSize > 0 {
r = append(r, fmt.Sprintf("list_text_size = %.2f\n", s.TextSize))
}
r = append(r, "list_use_relative_dates = "+pyBool(s.UseRelativeDates)+"\n")
r = append(
r, "list_calculate_all_sizes = "+pyBool(s.CalculateAllSizes)+"\n",
)
if len(s.Columns) > 0 {
var cols []string
for _, col := range s.Columns {
cols = append(cols, pyStr(string(col)))
}
r = append(r,
"list_columns = [\n "+strings.Join(cols, ",\n ")+"\n]\n",
)
}
if len(s.ColumnWidths) > 0 {
var cols []string
for col, w := range s.ColumnWidths {
cols = append(cols, fmt.Sprintf(
"%s: %d", pyStr(string(col)), w,
))
}
sort.SliceStable(cols, func(i, j int) bool {
return cols[i] < cols[j]
})
r = append(r,
"list_column_widths = {\n "+
strings.Join(cols, ",\n ")+
"\n}\n",
)
}
if len(s.ColumnSortDirections) > 0 {
var cols []string
for col, direction := range s.ColumnSortDirections {
cols = append(cols, fmt.Sprintf(
"%s: %s", pyStr(string(col)), pyStr(string(direction)),
))
}
sort.SliceStable(cols, func(i, j int) bool {
return cols[i] < cols[j]
})
r = append(r,
"list_column_sort_directions = {\n "+
strings.Join(cols, ",\n ")+
"\n}\n",
)
}
return r
}

256
pkg/dmgbuild/settings.go Normal file
View File

@@ -0,0 +1,256 @@
package dmgbuild
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/hashicorp/go-hclog"
)
type format string
//nolint:golint
var (
UDROFormat format = "UDRO" // Read-only
UDCOFormat format = "UDCO" // Compressed (ADC)
UDZOFormat format = "UDZO" // Compressed (gzip)
UDBZFormat format = "UDBZ" // Compressed (bzip2)
UFBIFormat format = "UFBI" // Entire device
IPODFormat format = "IPOD" // iPod image
UDxxFormat format = "UDxx" // UDIF stub
UDSBFormat format = "UDSB" // Sparse bundle
UDSPFormat format = "UDSP" // Sparse
UDRWFormat format = "UDRW" // Read/write
UDTOFormat format = "UDTO" // DVD/CD master
DC42Format format = "DC42" // Disk Copy 4.2
RdWrFormat format = "RdWr" // NDIF read/write
RdxxFormat format = "Rdxx" // NDIF read-only
ROCoFormat format = "ROCo" // NDIF Compressed
RkenFormat format = "Rken" // NDIF Compressed (KenCode)
)
type File struct {
Path string
PosX int
PosY int
Hidden bool
HideExtension bool
}
type Symlink struct {
Name string
Target string
PosX int
PosY int
Hidden bool
HideExtension bool
}
type Settings struct {
// Command can be set to a custom dmgbuild executable path. If not set,
// the first "dmgbuild" executable within PATH will be used.
Command string
// Stdout will be set as STDOUT target for dmgbuild execution if not nil.
Stdout io.Writer
// Stderr will be set as STDERR target for dmgbuild execution if not nil.
Stderr io.Writer
// Logger allows logging details of dmbuild process.
Logger hclog.Logger
// dmgbuild settings
Filename string
VolumeName string
Format format
Size string
CompressionLevel int
Files []*File
Symlinks []*Symlink
Icon string
BadgeIcon string
Window Window
IconView IconView
ListView ListView
License License
}
func NewSettings() *Settings {
return &Settings{
Format: UDZOFormat,
CompressionLevel: 9,
Window: NewWindow(),
IconView: NewIconView(),
ListView: NewListView(),
License: NewLicense(),
}
}
//nolint:funlen,gocyclo
// Render returns a string slice where each string is a separate settings
// statement.
func (s *Settings) Render() ([]string, error) {
r := []string{
"# -*- coding: utf-8 -*-\n",
"from __future__ import unicode_literals\n",
}
if s.Filename != "" {
r = append(r, "filename = "+pyStr(s.Filename)+"\n")
}
if s.VolumeName != "" {
r = append(r, "volume_name = "+pyStr(s.VolumeName)+"\n")
}
if s.Format != "" {
r = append(r, "format = "+pyStr(string(s.Format))+"\n")
}
if s.CompressionLevel != 0 {
r = append(r, fmt.Sprintf(
"compression_level = %d\n", s.CompressionLevel,
))
}
if s.Size != "" {
r = append(r, "size = "+pyStr(s.Size)+"\n")
}
var files []string
var symlinks []string
var hide []string
var hideExt []string
var iconLoc []string
if len(s.Files) > 0 {
for _, f := range s.Files {
files = append(files, pyStr(f.Path))
name := filepath.Base(f.Path)
if f.PosX > 0 || f.PosY > 0 {
iconLoc = append(iconLoc,
fmt.Sprintf("%s: (%d, %d)", pyStr(name), f.PosX, f.PosY),
)
}
if f.Hidden {
hide = append(hide, pyStr(filepath.Base(f.Path)))
}
if f.HideExtension {
hideExt = append(hideExt, pyStr(filepath.Base(f.Path)))
}
}
}
if len(s.Symlinks) > 0 {
for _, l := range s.Symlinks {
symlinks = append(symlinks, pyStr(l.Name)+": "+pyStr(l.Target))
if l.PosX > 0 || l.PosY > 0 {
iconLoc = append(iconLoc,
fmt.Sprintf("%s: (%d, %d)", pyStr(l.Name), l.PosX, l.PosY),
)
}
if l.Hidden {
hide = append(hide, pyStr(l.Name))
}
if l.HideExtension {
hideExt = append(hideExt, pyStr(l.Name))
}
}
}
if len(files) > 0 {
r = append(r,
"files = [\n "+strings.Join(files, ",\n ")+"\n]\n",
)
}
if len(symlinks) > 0 {
r = append(r,
"symlinks = {\n "+strings.Join(symlinks, ",\n ")+"\n}\n",
)
}
if len(hide) > 0 {
r = append(r,
"hide = [\n "+strings.Join(hide, ",\n ")+"\n]\n",
)
}
if len(hideExt) > 0 {
r = append(r,
"hide_extensions = [\n "+strings.Join(hideExt, ",\n ")+
"\n]\n",
)
}
if len(iconLoc) > 0 {
r = append(r,
"icon_locations = {\n "+strings.Join(iconLoc, ",\n ")+"\n}\n",
)
}
if s.Icon != "" {
r = append(r, "icon = "+pyStr(s.Icon)+"\n")
}
if s.BadgeIcon != "" {
r = append(r, "badge_icon = "+pyStr(s.BadgeIcon)+"\n")
}
r = append(r, s.Window.Render()...)
r = append(r, s.IconView.Render()...)
r = append(r, s.ListView.Render()...)
r = append(r, s.License.Render()...)
return r, nil
}
func (s *Settings) Write(w io.Writer) error {
out, err := s.Render()
if err != nil {
return err
}
for _, o := range out {
_, err := w.Write([]byte(o))
if err != nil {
return err
}
}
return nil
}
func (s *Settings) TempFile() (string, error) {
f, err := os.CreateTemp("", "*.dmgbuild.settings.py")
if err != nil {
return "", err
}
defer f.Close()
err = s.Write(f)
if err != nil {
return "", err
}
return f.Name(), nil
}
func pyStr(s string) string {
s = strings.ReplaceAll(s, `\`, `\\`)
s = strings.ReplaceAll(s, `"`, `\"`)
s = strings.ReplaceAll(s, "\r", `\r`)
s = strings.ReplaceAll(s, "\n", `\n`)
return `"` + s + `"`
}
func pyMStr(s string) string {
s = strings.ReplaceAll(s, `\`, `\\`)
s = strings.ReplaceAll(s, `"`, `\"`)
return `"""` + s + `"""`
}
func pyBool(v bool) string {
if v {
return "True"
}
return "False"
}

View File

@@ -0,0 +1,444 @@
package dmgbuild
import (
"bytes"
"strings"
"testing"
"github.com/jimeh/undent"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSettings_Write(t *testing.T) {
test := []struct {
name string
entitlements *Settings
want string
}{
{
name: "empty",
entitlements: &Settings{},
want: undent.String(`
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
show_status_bar = False
show_tab_view = False
show_toolbar = False
show_pathbar = False
show_sidebar = False
show_icon_preview = False
show_item_info = False
include_icon_view_settings = False
include_list_view_settings = False
list_use_relative_dates = False
list_calculate_all_sizes = False`,
),
},
{
name: "full",
entitlements: &Settings{
Filename: "/builds/Emacs.2021-05-25.f4dc646.master.dmg",
VolumeName: "Emacs.2021-05-25.f4dc646.master",
Format: UDBZFormat,
CompressionLevel: 8,
Size: "100m",
Files: []*File{
{
Path: "/builds/Emacs.app",
PosX: 200,
PosY: 200,
},
{
Path: "/builds/README.rtf",
PosX: 200,
PosY: 300,
HideExtension: true,
},
{
Path: "/builds/hide-me.png",
Hidden: true,
},
},
Symlinks: []*Symlink{
{
Name: "Applications",
Target: "/Applications",
PosX: 400,
PosY: 400,
},
{
Name: "QuickLook",
Target: "/Library/QuickLook",
PosX: 500,
PosY: 400,
Hidden: true,
},
{
Name: "System",
Target: "/System",
HideExtension: true,
},
},
Icon: "/opt/misc/assets/volIcon.icns",
BadgeIcon: "/builds/Emacs.app/Contents/Resources/Icon.icns",
Window: Window{
PoxX: 200,
PosY: 250,
Width: 680,
Height: 446,
Background: "/opt/misc/assets/bg.tif",
ShowStatusBar: true,
ShowTabView: true,
ShowToolbar: true,
ShowPathbar: true,
ShowSidebar: true,
SidebarWidth: 165,
DefaultView: list,
ShowIconPreview: true,
ShowItemInfo: true,
IncludeIconViewSettings: true,
IncludeListViewSettings: true,
},
IconView: IconView{
ArrangeBy: NameOrder,
GridOffsetX: 42,
GridOffsetY: 43,
GridSpacing: 44.5,
ScrollPosX: 4.5,
ScrollPosY: 5.5,
LabelPosition: LabelBottom,
IconSize: 160,
TextSize: 15,
},
ListView: ListView{
SortBy: NameColumn,
ScrollPosX: 7,
ScrollPosY: 8,
IconSize: 16,
TextSize: 12,
UseRelativeDates: true,
CalculateAllSizes: true,
Columns: []listColumn{
NameColumn,
DateModifiedColumn,
DateCreatedColumn,
DateAddedColumn,
DateLastOpenedColumn,
SizeColumn,
KindColumn,
LabelColumn,
VersionColumn,
CommentsColumn,
},
ColumnWidths: map[listColumn]int{
(NameColumn): 300,
(DateModifiedColumn): 181,
(DateCreatedColumn): 181,
(DateAddedColumn): 181,
(DateLastOpenedColumn): 181,
(SizeColumn): 97,
(KindColumn): 115,
(LabelColumn): 100,
(VersionColumn): 75,
(CommentsColumn): 300,
},
ColumnSortDirections: map[listColumn]direction{
(NameColumn): Ascending,
(DateModifiedColumn): Descending,
(DateCreatedColumn): Descending,
(DateAddedColumn): Descending,
(DateLastOpenedColumn): Descending,
(SizeColumn): Descending,
(KindColumn): Ascending,
(LabelColumn): Ascending,
(VersionColumn): Ascending,
(CommentsColumn): Ascending,
},
},
License: License{
DefaultLanguage: LocaleEnUS,
Licenses: map[locale]string{
//nolint:lll
(LocaleEnGB): undent.String(`
{\rtf1\ansi\ansicpg1252\cocoartf1504\cocoasubrtf820
{\fonttbl\f0\fnil\fcharset0 Helvetica-Bold;\f1\fnil\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;\red0\green0\blue0;}
{\*\expandedcolortbl;;\cssrgb\c0\c0\c0;}
\paperw11905\paperh16837\margl1133\margr1133\margb1133\margt1133
\deftab720
\pard\pardeftab720\sa160\partightenfactor0
\f0\b\fs60 \cf2 \expnd0\expndtw0\kerning0
\up0 \nosupersub \ulnone \outl0\strokewidth0 \strokec2 Test License\
\pard\pardeftab720\sa160\partightenfactor0
\fs36 \cf2 \strokec2 What is this?\
\pard\pardeftab720\sa160\partightenfactor0
\f1\b0\fs22 \cf2 \strokec2 This is the English license. It says what you are allowed to do with this software.\
\
}`,
),
//nolint:lll
(LocaleSe): undent.String(`
{\rtf1\ansi\ansicpg1252\cocoartf1504\cocoasubrtf820
{\fonttbl\f0\fnil\fcharset0 Helvetica-Bold;\f1\fnil\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;\red0\green0\blue0;}
{\*\expandedcolortbl;;\cssrgb\c0\c0\c0;}
\paperw11905\paperh16837\margl1133\margr1133\margb1133\margt1133
\deftab720
\pard\pardeftab720\sa160\partightenfactor0
\f0\b\fs60 \cf2 \expnd0\expndtw0\kerning0
\up0 \nosupersub \ulnone \outl0\strokewidth0 \strokec2 Test License\
\pard\pardeftab720\sa160\partightenfactor0
\fs36 \cf2 \strokec2 What is this?\
\pard\pardeftab720\sa160\partightenfactor0
\f1\b0\fs22 \cf2 \strokec2 Detta är den engelska licensen. Det står vad du får göra med den här programvaran.\
\
}`,
),
},
Buttons: map[locale]Buttons{
(LocaleEnGB): {
LanguageName: "English",
Agree: "Agree",
Disagree: "Disagree",
Print: "Print",
Save: "Save",
Message: "If you agree with the terms of this " +
"license, press \"Agree\" to install the " +
"software. If you do not agree, press " +
"\"Disagree\".",
},
(LocaleSe): {
LanguageName: "Svenska",
Agree: "Godkänn",
Disagree: "Håller inte med",
Print: "Skriv ut",
Save: "Spara",
Message: "Om du godkänner villkoren i denna " +
"licens, tryck på \"Godkänn\" för att " +
"installera programvaran. Om du inte håller " +
"med, tryck på \"Håller inte med\".",
},
},
},
},
//nolint:lll
want: undent.String(`
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
filename = "/builds/Emacs.2021-05-25.f4dc646.master.dmg"
volume_name = "Emacs.2021-05-25.f4dc646.master"
format = "UDBZ"
compression_level = 8
size = "100m"
files = [
"/builds/Emacs.app",
"/builds/README.rtf",
"/builds/hide-me.png"
]
symlinks = {
"Applications": "/Applications",
"QuickLook": "/Library/QuickLook",
"System": "/System"
}
hide = [
"hide-me.png",
"QuickLook"
]
hide_extensions = [
"README.rtf",
"System"
]
icon_locations = {
"Emacs.app": (200, 200),
"README.rtf": (200, 300),
"Applications": (400, 400),
"QuickLook": (500, 400)
}
icon = "/opt/misc/assets/volIcon.icns"
badge_icon = "/builds/Emacs.app/Contents/Resources/Icon.icns"
background = "/opt/misc/assets/bg.tif"
show_status_bar = True
show_tab_view = True
show_toolbar = True
show_pathbar = True
show_sidebar = True
sidebar_width = 165
default_view = "list-view"
window_rect = ((200, 250), (680, 446))
show_icon_preview = True
show_item_info = True
include_icon_view_settings = True
include_list_view_settings = True
arrange_by = "name"
grid_offset = (42, 43)
grid_spacing = 44.50
scroll_position = (4.50, 5.50)
label_position = "bottom"
icon_size = 160.00
text_size = 15.00
list_sort_by = "name"
list_scroll_position = (7, 8)
list_icon_size = 16.00
list_text_size = 12.00
list_use_relative_dates = True
list_calculate_all_sizes = True
list_columns = [
"name",
"date-modified",
"date-created",
"date-added",
"date-last-opened",
"size",
"kind",
"label",
"version",
"comments"
]
list_column_widths = {
"comments": 300,
"date-added": 181,
"date-created": 181,
"date-last-opened": 181,
"date-modified": 181,
"kind": 115,
"label": 100,
"name": 300,
"size": 97,
"version": 75
}
list_column_sort_directions = {
"comments": "ascending",
"date-added": "descending",
"date-created": "descending",
"date-last-opened": "descending",
"date-modified": "descending",
"kind": "ascending",
"label": "ascending",
"name": "ascending",
"size": "descending",
"version": "ascending"
}
license = {
"default-language": "en_US",
"licenses": {
"en_GB": """{\\rtf1\\ansi\\ansicpg1252\\cocoartf1504\\cocoasubrtf820
{\\fonttbl\\f0\\fnil\\fcharset0 Helvetica-Bold;\\f1\\fnil\\fcharset0 Helvetica;}
{\\colortbl;\\red255\\green255\\blue255;\\red0\\green0\\blue0;}
{\\*\\expandedcolortbl;;\\cssrgb\\c0\\c0\\c0;}
\\paperw11905\\paperh16837\\margl1133\\margr1133\\margb1133\\margt1133
\\deftab720
\\pard\\pardeftab720\\sa160\\partightenfactor0
\\f0\\b\\fs60 \\cf2 \\expnd0\\expndtw0\\kerning0
\\up0 \\nosupersub \\ulnone \\outl0\\strokewidth0 \\strokec2 Test License\\
\\pard\\pardeftab720\\sa160\\partightenfactor0
\\fs36 \\cf2 \\strokec2 What is this?\\
\\pard\\pardeftab720\\sa160\\partightenfactor0
\\f1\\b0\\fs22 \\cf2 \\strokec2 This is the English license. It says what you are allowed to do with this software.\\
\\
}""",
"se": """{\\rtf1\\ansi\\ansicpg1252\\cocoartf1504\\cocoasubrtf820
{\\fonttbl\\f0\\fnil\\fcharset0 Helvetica-Bold;\\f1\\fnil\\fcharset0 Helvetica;}
{\\colortbl;\\red255\\green255\\blue255;\\red0\\green0\\blue0;}
{\\*\\expandedcolortbl;;\\cssrgb\\c0\\c0\\c0;}
\\paperw11905\\paperh16837\\margl1133\\margr1133\\margb1133\\margt1133
\\deftab720
\\pard\\pardeftab720\\sa160\\partightenfactor0
\\f0\\b\\fs60 \\cf2 \\expnd0\\expndtw0\\kerning0
\\up0 \\nosupersub \\ulnone \\outl0\\strokewidth0 \\strokec2 Test License\\
\\pard\\pardeftab720\\sa160\\partightenfactor0
\\fs36 \\cf2 \\strokec2 What is this?\\
\\pard\\pardeftab720\\sa160\\partightenfactor0
\\f1\\b0\\fs22 \\cf2 \\strokec2 Detta är den engelska licensen. Det står vad du får göra med den här programvaran.\\
\\
}"""
},
"buttons": {
"en_GB": (
"English",
"Agree",
"Disagree",
"Print",
"Save",
"If you agree with the terms of this license, press \"Agree\" to install the software. If you do not agree, press \"Disagree\"."
),
"se": (
"Svenska",
"Godkänn",
"Håller inte med",
"Skriv ut",
"Spara",
"Om du godkänner villkoren i denna licens, tryck på \"Godkänn\" för att installera programvaran. Om du inte håller med, tryck på \"Håller inte med\"."
)
}
}`,
),
},
}
for _, tt := range test {
t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer
err := tt.entitlements.Write(&buf)
require.NoError(t, err)
assert.Equal(t, tt.want, strings.TrimSpace(buf.String()))
})
}
}
func Test_pyStr(t *testing.T) {
tests := []struct {
name string
s string
want string
}{
{
name: "empty",
s: "",
want: `""`,
},
{
name: "regular string",
s: "foo-bar nope :)",
want: `"foo-bar nope :)"`,
},
{
name: "with single quotes",
s: "john's lost 'flip-flop'",
want: `"john's lost 'flip-flop'"`,
},
{
name: "with double quotes",
s: `john has lost a "flip-flop"`,
want: `"john has lost a \"flip-flop\""`,
},
{
name: "with backslashes",
s: `C:\path\to\file.txt`,
want: `"C:\\path\\to\\file.txt"`,
},
{
name: "with line-feed",
s: "hello\nworld",
want: `"hello\nworld"`,
},
{
name: "with carriage return",
s: "hello\rworld",
want: `"hello\rworld"`,
},
{
name: "with backslashes, single and double quotes",
s: `john's "lost" C:\path\to\file.txt`,
want: `"john's \"lost\" C:\\path\\to\\file.txt"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := pyStr(tt.s)
assert.Equal(t, tt.want, got)
})
}
}

83
pkg/dmgbuild/window.go Normal file
View File

@@ -0,0 +1,83 @@
package dmgbuild
import "fmt"
type view string
//nolint:golint
var (
Icon view = "icon-view"
list view = "list-view"
Column view = "column-view"
Coverflow view = "coverflow"
)
type Window struct {
PoxX int
PosY int
Width int
Height int
Background string
ShowStatusBar bool
ShowTabView bool
ShowToolbar bool
ShowPathbar bool
ShowSidebar bool
SidebarWidth int
DefaultView view
ShowIconPreview bool
ShowItemInfo bool
IncludeIconViewSettings bool
IncludeListViewSettings bool
}
func NewWindow() Window {
return Window{
PoxX: 100,
PosY: 150,
Width: 640,
Height: 280,
Background: "builtin-arrow",
DefaultView: Icon,
}
}
func (s *Window) Render() []string {
r := []string{}
if s.Background != "" {
r = append(r, "background = "+pyStr(s.Background)+"\n")
}
r = append(r, "show_status_bar = "+pyBool(s.ShowStatusBar)+"\n")
r = append(r, "show_tab_view = "+pyBool(s.ShowTabView)+"\n")
r = append(r, "show_toolbar = "+pyBool(s.ShowToolbar)+"\n")
r = append(r, "show_pathbar = "+pyBool(s.ShowPathbar)+"\n")
r = append(r, "show_sidebar = "+pyBool(s.ShowSidebar)+"\n")
if s.SidebarWidth > 0 {
r = append(r, fmt.Sprintf(
"sidebar_width = %d\n", s.SidebarWidth,
))
}
if s.DefaultView != "" {
r = append(r, "default_view = "+pyStr(string(s.DefaultView))+"\n")
}
if s.Width > 0 && s.Height > 0 {
r = append(r, fmt.Sprintf(
"window_rect = ((%d, %d), (%d, %d))\n",
s.PoxX, s.PosY, s.Width, s.Height,
))
}
r = append(r, "show_icon_preview = "+pyBool(s.ShowIconPreview)+"\n")
r = append(r, "show_item_info = "+pyBool(s.ShowIconPreview)+"\n")
r = append(
r, "include_icon_view_settings = "+pyBool(s.ShowIconPreview)+"\n",
)
r = append(
r, "include_list_view_settings = "+pyBool(s.ShowIconPreview)+"\n",
)
return r
}

24
pkg/gh/gh.go Normal file
View File

@@ -0,0 +1,24 @@
package gh
import (
"context"
"os"
"github.com/google/go-github/v35/github"
"golang.org/x/oauth2"
)
func New(ctx context.Context, token string) *github.Client {
if token == "" {
token = os.Getenv("GITHUB_TOKEN")
}
if token == "" {
return github.NewClient(nil)
}
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
tc := oauth2.NewClient(ctx, ts)
return github.NewClient(tc)
}

98
pkg/notarize/notarize.go Normal file
View File

@@ -0,0 +1,98 @@
package notarize
import (
"context"
"os/exec"
"path/filepath"
"sync"
"time"
"github.com/hashicorp/go-hclog"
"github.com/mitchellh/gon/notarize"
"github.com/mitchellh/gon/staple"
)
type Options struct {
File string
BundleID string
Username string
Password string
Provider string
Staple bool
}
func Notarize(ctx context.Context, opts *Options) error {
logger := hclog.FromContext(ctx).Named("notarize")
notarizeOpts := &notarize.Options{
File: opts.File,
BundleId: opts.BundleID,
Username: opts.Username,
Password: opts.Password,
Provider: opts.Provider,
BaseCmd: exec.CommandContext(ctx, ""),
Status: &status{
Lock: &sync.Mutex{},
Logger: logger,
},
}
logger.Info("notarizing", "file", filepath.Base(opts.File))
info, err := notarize.Notarize(ctx, notarizeOpts)
if err != nil {
return err
}
logger.Info(
"notarization complete",
"status", info.Status,
"message", info.StatusMessage,
)
if opts.Staple {
logger.Info("stapling", "file", filepath.Base(opts.File))
err := staple.Staple(ctx, &staple.Options{
File: opts.File,
BaseCmd: exec.CommandContext(ctx, ""),
})
if err != nil {
return err
}
}
return nil
}
type status struct {
Lock *sync.Mutex
Logger hclog.Logger
lastStatusTime time.Time
}
func (s *status) Submitting() {
s.Lock.Lock()
defer s.Lock.Unlock()
s.Logger.Info("submitting file for notarization...")
}
func (s *status) Submitted(uuid string) {
s.Lock.Lock()
defer s.Lock.Unlock()
s.Logger.Info("submitted")
s.Logger.Debug("request", "uuid", uuid)
s.Logger.Info("waiting for result from Apple...")
}
func (s *status) Status(info notarize.Info) {
s.Lock.Lock()
defer s.Lock.Unlock()
if time.Now().After(s.lastStatusTime.Add(60 * time.Second)) {
s.lastStatusTime = time.Now()
s.Logger.Info("status update", "status", info.Status)
}
}

40
pkg/osinfo/osinfo.go Normal file
View File

@@ -0,0 +1,40 @@
package osinfo
import (
"os/exec"
"strings"
)
type OSInfo struct {
Name string `yaml:"name"`
Version string `yaml:"version"`
Arch string `yaml:"arch"`
}
func New() (*OSInfo, error) {
version, err := exec.Command("sw_vers", "-productVersion").CombinedOutput()
if err != nil {
return nil, err
}
arch, err := exec.Command("uname", "-m").CombinedOutput()
if err != nil {
return nil, err
}
return &OSInfo{
Name: "macOS",
Version: strings.TrimSpace(string(version)),
Arch: strings.TrimSpace(string(arch)),
}, nil
}
func (s *OSInfo) MajorMinor() string {
parts := strings.Split(s.Version, ".")
max := len(parts)
if max > 2 {
max = 2
}
return strings.Join(parts[0:max], ".")
}

141
pkg/plan/create.go Normal file
View File

@@ -0,0 +1,141 @@
package plan
import (
"context"
"fmt"
"io"
"regexp"
"strings"
"github.com/hashicorp/go-hclog"
"github.com/jimeh/build-emacs-for-macos/pkg/commit"
"github.com/jimeh/build-emacs-for-macos/pkg/gh"
"github.com/jimeh/build-emacs-for-macos/pkg/osinfo"
"github.com/jimeh/build-emacs-for-macos/pkg/release"
"github.com/jimeh/build-emacs-for-macos/pkg/repository"
)
var nonAlphaNum = regexp.MustCompile(`[^\w_-]+`)
type TestBuildType string
//nolint:golint
const (
Draft TestBuildType = "draft"
Prerelease TestBuildType = "prerelease"
)
type Options struct {
GithubToken string
EmacsRepo string
Ref string
SHAOverride string
OutputDir string
TestBuild string
TestBuildType TestBuildType
Output io.Writer
}
func Create(ctx context.Context, opts *Options) (*Plan, error) {
logger := hclog.FromContext(ctx).Named("plan")
repo, err := repository.NewGitHub(opts.EmacsRepo)
if err != nil {
return nil, err
}
gh := gh.New(ctx, opts.GithubToken)
lookupRef := opts.Ref
if opts.SHAOverride != "" {
lookupRef = opts.SHAOverride
}
logger.Info("fetching commit info", "ref", lookupRef)
repoCommit, _, err := gh.Repositories.GetCommit(
ctx, repo.Owner(), repo.Name(), lookupRef,
)
if err != nil {
return nil, err
}
commitInfo := commit.New(repoCommit)
osInfo, err := osinfo.New()
if err != nil {
return nil, err
}
version := fmt.Sprintf(
"%s.%s.%s",
commitInfo.DateString(),
commitInfo.ShortSHA(),
sanitizeString(opts.Ref),
)
releaseName := fmt.Sprintf("Emacs.%s", version)
buildName := fmt.Sprintf(
"Emacs.%s.%s.%s",
version,
sanitizeString(osInfo.Name+"-"+osInfo.MajorMinor()),
sanitizeString(osInfo.Arch),
)
diskImage := buildName + ".dmg"
plan := &Plan{
Build: &Build{
Name: buildName,
},
Source: &Source{
Ref: opts.Ref,
Repository: repo,
Commit: commitInfo,
Tarball: &Tarball{
URL: repo.TarballURL(commitInfo.SHA),
},
},
OS: osInfo,
Release: &Release{
Name: releaseName,
Prerelease: true,
},
Output: &Output{
Directory: opts.OutputDir,
DiskImage: diskImage,
},
}
// If given git ref is a stable release tag (emacs-23.2b, emacs-27.2, etc.)
// we modify release properties accordingly.
if v, err := release.GitRefToStableVersion(opts.Ref); err == nil {
plan.Release.Prerelease = false
plan.Release.Name, err = release.VersionToName(v)
if err != nil {
return nil, err
}
}
if opts.TestBuild != "" {
testName := sanitizeString(opts.TestBuild)
plan.Build.Name += ".test." + testName
plan.Release.Title = "Test Builds"
plan.Release.Name = "test-builds"
plan.Release.Prerelease = true
plan.Release.Draft = false
if opts.TestBuildType == Draft {
plan.Release.Prerelease = false
plan.Release.Draft = true
}
index := strings.LastIndex(diskImage, ".")
plan.Output.DiskImage = diskImage[:index] + ".test." +
testName + diskImage[index:]
}
return plan, nil
}
func sanitizeString(s string) string {
return nonAlphaNum.ReplaceAllString(s, "-")
}

82
pkg/plan/plan.go Normal file
View File

@@ -0,0 +1,82 @@
package plan
import (
"bytes"
"io"
"os"
"github.com/jimeh/build-emacs-for-macos/pkg/commit"
"github.com/jimeh/build-emacs-for-macos/pkg/osinfo"
"github.com/jimeh/build-emacs-for-macos/pkg/repository"
"gopkg.in/yaml.v3"
)
type Plan struct {
Build *Build `yaml:"build,omitempty"`
Source *Source `yaml:"source,omitempty"`
OS *osinfo.OSInfo `yaml:"os,omitempty"`
Release *Release `yaml:"release,omitempty"`
Output *Output `yaml:"output,omitempty"`
}
// Load attempts to loads a plan YAML from given filename.
func Load(filename string) (*Plan, error) {
b, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
p := &Plan{}
err = yaml.Unmarshal(b, p)
if err != nil {
return nil, err
}
return p, nil
}
// WriteYAML writes plan in YAML format to given io.Writer.
func (s *Plan) WriteYAML(w io.Writer) error {
enc := yaml.NewEncoder(w)
enc.SetIndent(2)
return enc.Encode(s)
}
// YAML returns plan in YAML format.
func (s *Plan) YAML() (string, error) {
var buf bytes.Buffer
err := s.WriteYAML(&buf)
if err != nil {
return "", err
}
return buf.String(), nil
}
type Build struct {
Name string `yaml:"name,omitempty"`
}
type Source struct {
Ref string `yaml:"ref,omitempty"`
Repository *repository.Repository `yaml:"repository,omitempty"`
Commit *commit.Commit `yaml:"commit,omitempty"`
Tarball *Tarball `yaml:"tarball,omitempty"`
}
type Tarball struct {
URL string `yaml:"url,omitempty"`
}
type Release struct {
Name string `yaml:"name"`
Title string `yaml:"title,omitempty"`
Draft bool `yaml:"draft,omitempty"`
Prerelease bool `yaml:"prerelease,omitempty"`
}
type Output struct {
Directory string `yaml:"directory,omitempty"`
DiskImage string `yaml:"disk_image,omitempty"`
}

84
pkg/release/bulk.go Normal file
View File

@@ -0,0 +1,84 @@
package release
import (
"context"
"regexp"
"github.com/google/go-github/v35/github"
"github.com/hashicorp/go-hclog"
"github.com/jimeh/build-emacs-for-macos/pkg/gh"
"github.com/jimeh/build-emacs-for-macos/pkg/repository"
)
type BulkOptions struct {
Repository *repository.Repository
NamePattern string
Prerelease *bool
DryRun bool
GithubToken string
}
func Bulk(ctx context.Context, opts *BulkOptions) error {
logger := hclog.FromContext(ctx).Named("release")
gh := gh.New(ctx, opts.GithubToken)
nameMatcher, err := regexp.Compile(opts.NamePattern)
if err != nil {
return err
}
nextPage := 1
lastPage := 1
for nextPage <= lastPage {
releases, resp, err := gh.Repositories.ListReleases(
ctx, opts.Repository.Owner(), opts.Repository.Name(),
&github.ListOptions{
Page: nextPage,
PerPage: 100,
},
)
if err != nil {
return err
}
nextPage = resp.NextPage
lastPage = resp.LastPage
for _, r := range releases {
if !nameMatcher.MatchString(r.GetName()) {
continue
}
logger.Info("match found", "release", r.GetName())
var changes []interface{}
if opts.Prerelease != nil && r.GetPrerelease() != *opts.Prerelease {
changes = append(changes, "prerelease", *opts.Prerelease)
r.Prerelease = opts.Prerelease
}
if len(changes) > 0 {
changes = append(
[]interface{}{"release", r.GetName()}, changes...,
)
logger.Info("modifying", changes...)
if !opts.DryRun {
_, _, err = gh.Repositories.EditRelease(
ctx, opts.Repository.Owner(), opts.Repository.Name(),
r.GetID(), r,
)
if err != nil {
return err
}
}
}
}
if nextPage == 0 || lastPage == 0 {
break
}
}
return nil
}

80
pkg/release/check.go Normal file
View File

@@ -0,0 +1,80 @@
package release
import (
"context"
"fmt"
"net/http"
"path/filepath"
"strings"
"github.com/hashicorp/go-hclog"
"github.com/jimeh/build-emacs-for-macos/pkg/gh"
"github.com/jimeh/build-emacs-for-macos/pkg/repository"
)
type CheckOptions struct {
// Repository is the GitHub repository to check.
Repository *repository.Repository
// ReleaseName is the name of the GitHub Release to check.
ReleaseName string
// AssetFiles is a list of files which must all exist in the release for
// the check to pass.
AssetFiles []string
// GitHubToken is the OAuth token used to talk to the GitHub API.
GithubToken string
}
// Check checks if a GitHub repository has a Release by given name, and if the
// release contains assets with given filenames.
func Check(ctx context.Context, opts *CheckOptions) error {
logger := hclog.FromContext(ctx).Named("release")
gh := gh.New(ctx, opts.GithubToken)
repo := opts.Repository
release, resp, err := gh.Repositories.GetReleaseByTag(
ctx, repo.Owner(), repo.Name(), opts.ReleaseName,
)
if err != nil {
if resp.StatusCode == http.StatusNotFound {
return fmt.Errorf("release %s does not exist", opts.ReleaseName)
}
return err
}
logger.Info("release exists", "release", opts.ReleaseName)
missingMap := map[string]bool{}
for _, filename := range opts.AssetFiles {
filename = filepath.Base(filename)
missingMap[filename] = true
for _, a := range release.Assets {
if a.GetName() == filename {
logger.Info("asset exists", "filename", filename)
delete(missingMap, filename)
break
}
}
}
if len(missingMap) == 0 {
return nil
}
var missing []string
for f := range missingMap {
missing = append(missing, f)
}
logger.Error("missing assets", "filenames", missing)
return fmt.Errorf(
"release %s is missing assets:\n- %s",
opts.ReleaseName, strings.Join(missing, "\n-"),
)
}

242
pkg/release/publish.go Normal file
View File

@@ -0,0 +1,242 @@
package release
import (
"context"
"fmt"
"net/http"
"os"
"path/filepath"
"github.com/google/go-github/v35/github"
"github.com/hashicorp/go-hclog"
"github.com/jimeh/build-emacs-for-macos/pkg/gh"
"github.com/jimeh/build-emacs-for-macos/pkg/repository"
)
type releaseType int
// Release type constants
const (
Normal releaseType = iota
Draft
Prerelease
)
type PublishOptions struct {
// Repository is the GitHub repository to publish the release on.
Repository *repository.Repository
// CommitRef is the git commit ref to create the release tag on.
CommitRef string
// ReleaseName is the name of the git tag to create the release on.
ReleaseName string
// ReleaseTitle is the title of the release, if not specified ReleaseName
// will be used.
ReleaseTitle string
// ReleaseType is the type of release to create (normal, prerelease, or
// draft)
ReleaseType releaseType
// AssetFiles is a list of files which must all exist in the release for
// the check to pass.
AssetFiles []string
// GitHubToken is the OAuth token used to talk to the GitHub API.
GithubToken string
}
//nolint:funlen,gocyclo
// Publish creates and publishes a GitHub release.
func Publish(ctx context.Context, opts *PublishOptions) error {
logger := hclog.FromContext(ctx).Named("release")
gh := gh.New(ctx, opts.GithubToken)
files, err := publishFileList(opts.AssetFiles)
if err != nil {
return err
}
tagName := opts.ReleaseName
name := opts.ReleaseTitle
if name == "" {
name = tagName
}
prerelease := opts.ReleaseType == Prerelease
draft := opts.ReleaseType == Draft
logger.Info("checking release", "tag", tagName)
release, resp, err := gh.Repositories.GetReleaseByTag(
ctx, opts.Repository.Owner(), opts.Repository.Name(), tagName,
)
if err != nil {
if resp.StatusCode != http.StatusNotFound {
return err
}
logger.Info("creating release", "tag", tagName, "name", name)
release, _, err = gh.Repositories.CreateRelease(
ctx, opts.Repository.Owner(), opts.Repository.Name(),
&github.RepositoryRelease{
Name: &name,
TagName: &tagName,
TargetCommitish: &opts.CommitRef,
Prerelease: boolPtr(false),
Draft: boolPtr(true),
},
)
if err != nil {
return err
}
}
for _, fileName := range files {
fileIO, err2 := os.Open(fileName)
if err2 != nil {
return err2
}
defer fileIO.Close()
fileInfo, err2 := fileIO.Stat()
if err2 != nil {
return err2
}
fileBaseName := filepath.Base(fileName)
assetExists := false
for _, a := range release.Assets {
if a.GetName() != fileBaseName {
continue
}
if a.GetSize() == int(fileInfo.Size()) {
logger.Info("asset exists with correct size",
"file", fileBaseName,
"local_size", byteCountIEC(fileInfo.Size()),
"remote_size", byteCountIEC(int64(a.GetSize())),
)
assetExists = true
} else {
_, err = gh.Repositories.DeleteReleaseAsset(
ctx, opts.Repository.Owner(), opts.Repository.Name(),
a.GetID(),
)
if err != nil {
return err
}
logger.Info(
"deleted asset with wrong size", "file", fileBaseName,
)
}
}
if !assetExists {
logger.Info("uploading asset",
"file", fileBaseName,
"size", byteCountIEC(fileInfo.Size()),
)
_, _, err2 = gh.Repositories.UploadReleaseAsset(
ctx, opts.Repository.Owner(), opts.Repository.Name(),
release.GetID(),
&github.UploadOptions{Name: fileBaseName},
fileIO,
)
if err2 != nil {
return err2
}
}
}
changed := false
if release.GetName() != name {
release.Name = &name
changed = true
}
if release.GetDraft() != draft {
release.Draft = &draft
changed = true
}
if !draft && release.GetPrerelease() != prerelease {
release.Prerelease = &prerelease
changed = true
}
if changed {
release, _, err = gh.Repositories.EditRelease(
ctx, opts.Repository.Owner(), opts.Repository.Name(),
release.GetID(), release,
)
if err != nil {
return err
}
}
logger.Info("release created", "url", release.GetHTMLURL())
return nil
}
func publishFileList(files []string) ([]string, error) {
var output []string
for _, file := range files {
var err error
file, err = filepath.Abs(file)
if err != nil {
return nil, err
}
stat, err := os.Stat(file)
if err != nil {
return nil, err
}
if !stat.Mode().IsRegular() {
return nil, fmt.Errorf("\"%s\" is not a file", file)
}
output = append(output, file)
sumFile := file + ".sha256"
_, err = os.Stat(sumFile)
fmt.Printf("err: %+v\n", err)
if err != nil {
if os.IsNotExist(err) {
continue
}
return nil, err
}
output = append(output, sumFile)
}
return output, nil
}
func byteCountIEC(b int64) string {
const unit = 1024
if b < unit {
if b == 1 {
return fmt.Sprintf("%d byte", b)
}
return fmt.Sprintf("%d bytes", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %ciB",
float64(b)/float64(div), "KMGTPE"[exp])
}
func boolPtr(v bool) *bool {
return &v
}

42
pkg/release/version.go Normal file
View File

@@ -0,0 +1,42 @@
package release
import (
"errors"
"fmt"
"regexp"
)
// Errors
var (
Err = errors.New("release")
ErrInvalidName = fmt.Errorf("%w: invalid name", Err)
ErrEmptyVersion = fmt.Errorf("%w: empty version", Err)
ErrNotStableRef = fmt.Errorf(
"%w: git ref is not stable tagged release", Err,
)
)
var (
stableVersion = regexp.MustCompile(`^\d+\.\d+(?:[a-z]+)?$`)
stableGetRef = regexp.MustCompile(`^emacs-(\d+\.\d+(?:[a-z]+)?)$`)
)
func VersionToName(version string) (string, error) {
if version == "" {
return "", ErrEmptyVersion
}
if stableVersion.MatchString(version) {
return "Emacs-" + version, nil
}
return "Emacs." + version, nil
}
func GitRefToStableVersion(ref string) (string, error) {
if m := stableGetRef.FindStringSubmatch(ref); len(m) > 1 {
return m[1], nil
}
return "", fmt.Errorf("%w: \"%s\"", ErrNotStableRef, ref)
}

138
pkg/release/version_test.go Normal file
View File

@@ -0,0 +1,138 @@
package release
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestVersionToName(t *testing.T) {
type args struct {
version string
}
tests := []struct {
name string
args args
want string
wantErr string
}{
{
name: "empty",
args: args{
version: "",
},
wantErr: "release: empty version",
},
{
name: "nightly",
args: args{
version: "2021-07-01.1b88404.master",
},
want: "Emacs.2021-07-01.1b88404.master",
},
{
name: "stable",
args: args{
version: "27.2",
},
want: "Emacs-27.2",
},
{
name: "stable with letter",
args: args{
version: "23.3b",
},
want: "Emacs-23.3b",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := VersionToName(tt.args.version)
assert.Equal(t, tt.want, got)
if tt.wantErr != "" {
assert.EqualError(t, err, tt.wantErr)
} else {
assert.NoError(t, err)
}
})
}
}
func TestGitRefToStableVersion(t *testing.T) {
type args struct {
version string
}
tests := []struct {
name string
args args
want string
wantErr string
}{
{
name: "empty",
args: args{
version: "",
},
wantErr: "release: git ref is not stable tagged release: \"\"",
},
{
name: "master",
args: args{
version: "master",
},
wantErr: "release: git ref is not stable tagged release: " +
"\"master\"",
},
{
name: "feature",
args: args{
version: "feature/native-comp",
},
wantErr: "release: git ref is not stable tagged release: " +
"\"feature/native-comp\"",
},
{
name: "stable",
args: args{
version: "emacs-27.2",
},
want: "27.2",
},
{
name: "stable with letter",
args: args{
version: "emacs-23.3b",
},
want: "23.3b",
},
{
name: "future stable",
args: args{
version: "emacs-239.33",
},
want: "239.33",
},
{
name: "future stable with letter",
args: args{
version: "emacs-239.33c",
},
want: "239.33c",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GitRefToStableVersion(tt.args.version)
assert.Equal(t, tt.want, got)
if tt.wantErr != "" {
assert.EqualError(t, err, tt.wantErr)
} else {
assert.NoError(t, err)
}
})
}
}

View File

@@ -0,0 +1,91 @@
package repository
import (
"errors"
"fmt"
"strings"
)
//nolint:golint
var (
Err = errors.New("repository")
ErrGitHub = fmt.Errorf("%w: github", Err)
)
const GitHubBaseURL = "https://github.com/"
// Type is a repository type
type Type string
const GitHub Type = "github"
// Repository represents basic information about a repository with helper
// methods to get various pieces of information from it.
type Repository struct {
Type Type `yaml:"type,omitempty"`
Source string `yaml:"source,omitempty"`
}
func NewGitHub(ownerAndName string) (*Repository, error) {
parts := strings.Split(ownerAndName, "/")
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return nil, fmt.Errorf(
"%w: repository must be give in \"owner/name\" format",
ErrGitHub,
)
}
return &Repository{
Type: GitHub,
Source: ownerAndName,
}, nil
}
func (s *Repository) Owner() string {
switch s.Type {
case GitHub:
return strings.SplitN(s.Source, "/", 2)[0]
default:
return ""
}
}
func (s *Repository) Name() string {
switch s.Type {
case GitHub:
return strings.SplitN(s.Source, "/", 2)[1]
default:
return ""
}
}
func (s *Repository) URL() string {
switch s.Type {
case GitHub:
return GitHubBaseURL + s.Source
default:
return ""
}
}
func (s *Repository) CloneURL() string {
switch s.Type {
case GitHub:
return GitHubBaseURL + s.Source + ".git"
default:
return ""
}
}
func (s *Repository) TarballURL(ref string) string {
if ref == "" {
return ""
}
switch s.Type {
case GitHub:
return GitHubBaseURL + s.Source + "/tarball/" + ref
default:
return ""
}
}

141
pkg/sign/emacs.go Normal file
View File

@@ -0,0 +1,141 @@
package sign
import (
"context"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/hashicorp/go-hclog"
)
// Emacs signs a Emacs.app application bundle with Apple's codesign utility,
// using correct default entitlements, and also pre-signing any *.eln files
// which are in the bundle, as codesign will not detect them as requiring
// signing even with the --deep flag.
func Emacs(ctx context.Context, appBundle string, opts *Options) error {
if !strings.HasSuffix(appBundle, ".app") {
return fmt.Errorf("%s is not a .app application bundle", appBundle)
}
appBundle, err := filepath.Abs(appBundle)
if err != nil {
return err
}
_, err = os.Stat(appBundle)
if err != nil {
return err
}
logger := hclog.FromContext(ctx).Named("sign")
logger.Info("preparing to sign Emacs.app", "app", appBundle)
newOpts := *opts
if newOpts.EntitlementsFile == "" {
if newOpts.Entitlements == nil {
e := Entitlements(DefaultEmacsEntitlements)
newOpts.Entitlements = &e
}
f, err2 := newOpts.Entitlements.TempFile()
if err2 != nil {
return err2
}
defer os.Remove(f)
newOpts.EntitlementsFile = f
newOpts.Entitlements = nil
}
err = signElnFiles(ctx, appBundle, &newOpts)
if err != nil {
return err
}
err = signCLIHelper(ctx, appBundle, &newOpts)
if err != nil {
return err
}
// Ensure app bundle is signed last, as modifications to the bundle after
// signing will invalidate the signature. Hence anything within it that
// needs to be separately signed, has to happen before signing the whole
// application bundle.
return Files(ctx, []string{appBundle}, &newOpts)
}
func signElnFiles(ctx context.Context, appBundle string, opts *Options) error {
logger := hclog.FromContext(ctx).Named("sign")
elnFiles, err := elnFiles(appBundle)
if err != nil {
return err
}
if len(elnFiles) == 0 {
return nil
}
logger.Info(fmt.Sprintf(
"found %d native-lisp *.eln files in %s to sign",
len(elnFiles), filepath.Base(appBundle),
))
for _, file := range elnFiles {
err := Files(ctx, []string{file}, opts)
if err != nil {
return err
}
}
return nil
}
func signCLIHelper(ctx context.Context, appBundle string, opts *Options) error {
logger := hclog.FromContext(ctx).Named("sign")
cliHelper := filepath.Join(appBundle, "Contents", "MacOS", "bin", "emacs")
fi, err := os.Stat(cliHelper)
if err != nil && !os.IsNotExist(err) {
return err
} else if err == nil && fi.Mode().IsRegular() {
logger.Info(fmt.Sprintf(
"found Contents/MacOS/bin/emacs CLI helper script in %s to sign",
filepath.Base(appBundle),
))
err = Files(ctx, []string{cliHelper}, opts)
if err != nil {
return err
}
}
return nil
}
// elnFiles finds all native-compilation *.eln files within a Emacs.app bundle,
// excluding any *.eln which should be automatically located by codesign when
// signing the Emacs.app bundle itself with the --deep flag. Essentially this
// only returns *.eln files which must be individually signed before signing the
// app bundle itself.
func elnFiles(emacsApp string) ([]string, error) {
var files []string
walkDirFunc := func(path string, d fs.DirEntry, _err error) error {
if d.Type().IsRegular() && strings.HasSuffix(path, ".eln") &&
!strings.Contains(path, ".app/Contents/Frameworks/") {
files = append(files, path)
}
return nil
}
err := filepath.WalkDir(filepath.Join(emacsApp, "Contents"), walkDirFunc)
if err != nil {
return nil, err
}
return files, nil
}

54
pkg/sign/entitlements.go Normal file
View File

@@ -0,0 +1,54 @@
package sign
import (
"bytes"
_ "embed"
"io"
"os"
"text/template"
)
// DefaultEmacsEntitlements is the default set of entitlements application
// bundles are signed with if no entitlements are provided.
var DefaultEmacsEntitlements = []string{
"com.apple.security.cs.allow-jit",
"com.apple.security.network.client",
"com.apple.security.cs.disable-library-validation",
"com.apple.security.automation.apple-events",
}
//go:embed entitlements.tpl
var entitlementsTemplate string
type Entitlements []string
func (e Entitlements) XML() ([]byte, error) {
var buf bytes.Buffer
err := e.Write(&buf)
return buf.Bytes(), err
}
func (e Entitlements) Write(w io.Writer) error {
tpl, err := template.New("entitlements.plist").Parse(entitlementsTemplate)
if err != nil {
return err
}
return tpl.Execute(w, e)
}
func (e Entitlements) TempFile() (string, error) {
f, err := os.CreateTemp("", "*.entitlements.plist")
if err != nil {
return "", err
}
defer f.Close()
err = e.Write(f)
if err != nil {
return "", err
}
return f.Name(), nil
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
{{- range . }}
<key>{{ . }}</key>
<true/>{{ end }}
</dict>
</plist>

View File

@@ -0,0 +1,117 @@
package sign
import (
"bytes"
"os"
"strings"
"testing"
"github.com/jimeh/undent"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var entitlementsTestCases = []struct {
name string
entitlements Entitlements
want string
}{
{
name: "none",
entitlements: Entitlements{},
//nolint:lll
want: undent.String(`
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
</dict>
</plist>`,
),
},
{
name: "one",
entitlements: Entitlements{"com.apple.security.cs.allow-jit"},
//nolint:lll
want: undent.String(`
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
</dict>
</plist>`,
),
},
{
name: "many",
entitlements: Entitlements{
"com.apple.security.cs.allow-jit",
"com.apple.security.network.client",
"com.apple.security.cs.disable-library-validation",
"com.apple.security.automation.apple-events",
},
//nolint:lll
want: undent.String(`
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.automation.apple-events</key>
<true/>
</dict>
</plist>`,
),
},
}
func TestDefaultEmacsEntitlements(t *testing.T) {
assert.Equal(t,
[]string{
"com.apple.security.cs.allow-jit",
"com.apple.security.network.client",
"com.apple.security.cs.disable-library-validation",
"com.apple.security.automation.apple-events",
},
DefaultEmacsEntitlements,
)
}
func TestEntitlements_Write(t *testing.T) {
for _, tt := range entitlementsTestCases {
t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer
err := tt.entitlements.Write(&buf)
require.NoError(t, err)
assert.Equal(t, tt.want, strings.TrimSpace(buf.String()))
})
}
}
func TestEntitlements_TempFile(t *testing.T) {
for _, tt := range entitlementsTestCases {
t.Run(tt.name, func(t *testing.T) {
tmpFile, err := tt.entitlements.TempFile()
require.NoError(t, err)
defer os.Remove(tmpFile)
content, err := os.ReadFile(tmpFile)
require.NoError(t, err)
assert.Equal(t, tt.want, strings.TrimSpace(string(content)))
assert.True(t,
strings.HasSuffix(tmpFile, ".entitlements.plist"),
"temp file name does not match \"*.entitlements.plist\"",
)
})
}
}

67
pkg/sign/files.go Normal file
View File

@@ -0,0 +1,67 @@
package sign
import (
"context"
"os"
"os/exec"
"strings"
"github.com/hashicorp/go-hclog"
)
func Files(ctx context.Context, files []string, opts *Options) error {
logger := hclog.FromContext(ctx).Named("sign")
args := []string{}
if opts.Identity != "" {
args = append(args, "--sign", opts.Identity)
}
if opts.Deep {
args = append(args, "--deep")
}
if opts.Timestamp {
args = append(args, "--timestamp")
}
if opts.Force {
args = append(args, "--force")
}
if opts.Verbose {
args = append(args, "--verbose")
}
if len(opts.Options) > 0 {
args = append(args, "--options", strings.Join(opts.Options, ","))
}
if opts.EntitlementsFile != "" {
args = append(args, "--entitlements", opts.EntitlementsFile)
} else if opts.Entitlements != nil {
entitlementsFile, err := opts.Entitlements.TempFile()
if err != nil {
return err
}
defer os.Remove(entitlementsFile)
logger.Debug("wrote entitlements", "file", entitlementsFile)
args = append(args, "--entitlements", entitlementsFile)
}
baseCmd := opts.CodeSignCmd
if baseCmd == "" {
path, err := exec.LookPath("codesign")
if err != nil {
return err
}
baseCmd = path
}
args = append(args, files...)
logger.Debug("executing", "command", baseCmd, "args", args)
cmd := exec.CommandContext(ctx, baseCmd, args...)
if opts.Output != nil {
cmd.Stdout = opts.Output
cmd.Stderr = opts.Output
}
return cmd.Run()
}

16
pkg/sign/options.go Normal file
View File

@@ -0,0 +1,16 @@
package sign
import "io"
type Options struct {
Identity string
Entitlements *Entitlements
EntitlementsFile string
Options []string
Deep bool
Timestamp bool
Force bool
Verbose bool
Output io.Writer
CodeSignCmd string
}

1
requirements-ci.txt Normal file
View File

@@ -0,0 +1 @@
dmgbuild