diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c349108 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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" diff --git a/.gitignore b/.gitignore index f75fd16..1c59c21 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .DS_Store +.envrc Formula/* Gemfile.lock +bin builds sources tarballs diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..34fe368 --- /dev/null +++ b/.golangci.yml @@ -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 diff --git a/Brewfile.ci b/Brewfile.ci new file mode 100644 index 0000000..638e9f6 --- /dev/null +++ b/Brewfile.ci @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +brew 'python' diff --git a/Makefile b/Makefile index fe08d61..a3351b1 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index b29ed04..3389857 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,11 @@ Options: --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: /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 ``` diff --git a/build-emacs-for-macos b/build-emacs-for-macos index 2e22fbd..241e688 100755 --- a/build-emacs-for-macos +++ b/build-emacs-for-macos @@ -101,13 +101,15 @@ class Build detect_native_comp if options[:native_comp].nil? app = compile_source(@source_dir) + build_dir, app = create_build_dir(app) + symlink_internals(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 @@ -116,11 +118,24 @@ class Build plan = YAML.safe_load(File.read(filename), [:Time]) @meta = { - sha: plan.dig('commit', 'sha'), - ref: plan.dig('commit', 'ref'), - date: plan.dig('commit', 'date') + sha: plan.dig('source', 'commit', 'sha'), + ref: plan.dig('source', '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 +146,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 @@ -363,6 +378,33 @@ class Build emacs_app end + 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 symlink_internals(app) return unless options[:native_comp] @@ -397,39 +439,47 @@ class Build FileUtils.chmod('+w', target) end - def archive_filename - return @archive_filename if @archive_filename + def build_name + return @build_name if @build_name + return @build_name = options[:build_name] if options[:build_name] metadata = [ - meta[:ref]&.gsub(/\W/, '-'), meta[:date]&.strftime('%Y-%m-%d'), meta[:sha][0..6], + meta[:ref], "macOS-#{OS.version}", OS.arch - ].compact + ].compact.map { |v| v.gsub(/[^\w_-]+/, '-') } - filename = "Emacs.app-[#{metadata.join('][')}].tbz" - @archive_filename = File.join(builds_dir, filename) + @build_name = "Emacs.#{metadata.join('.')}" end - def archive_app(app) + 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 @@ -908,7 +958,10 @@ if __FILE__ == $PROGRAM_NAME parallel: Etc.nprocessors, rsvg: true, xwidgets: true, - github_auth: true + github_auth: true, + dist_include: ['COPYING'], + archive: true, + archive_keep: false } begin @@ -986,6 +1039,33 @@ if __FILE__ == $PROGRAM_NAME cli_options[:work_dir] = v end + opts.on('-o DIR', '--output DIR', + 'Output directory for finished builds ' \ + '(default: /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' diff --git a/cmd/emacs-builder/main.go b/cmd/emacs-builder/main.go new file mode 100644 index 0000000..a57fac5 --- /dev/null +++ b/cmd/emacs-builder/main.go @@ -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) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8a6f862 --- /dev/null +++ b/go.mod @@ -0,0 +1,29 @@ +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/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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9602a2c --- /dev/null +++ b/go.sum @@ -0,0 +1,468 @@ +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/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= diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go new file mode 100644 index 0000000..9d54855 --- /dev/null +++ b/pkg/cli/cli.go @@ -0,0 +1,94 @@ +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(), + { + 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) +} diff --git a/pkg/cli/notarize.go b/pkg/cli/notarize.go new file mode 100644 index 0000000..1abc44f --- /dev/null +++ b/pkg/cli/notarize.go @@ -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: "", + 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 := ¬arize.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) +} diff --git a/pkg/cli/options.go b/pkg/cli/options.go new file mode 100644 index 0000000..688bb1a --- /dev/null +++ b/pkg/cli/options.go @@ -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) + } +} diff --git a/pkg/cli/package.go b/pkg/cli/package.go new file mode 100644 index 0000000..bc6243c --- /dev/null +++ b/pkg/cli/package.go @@ -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: "", + 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 := ¬arize.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 +} diff --git a/pkg/cli/plan.go b/pkg/cli/plan.go new file mode 100644 index 0000000..7c3cfd8 --- /dev/null +++ b/pkg/cli/plan.go @@ -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: "", + 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-build-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 +} diff --git a/pkg/cli/release.go b/pkg/cli/release.go new file mode 100644 index 0000000..7b01074 --- /dev/null +++ b/pkg/cli/release.go @@ -0,0 +1,208 @@ +package cli + +import ( + "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(), + }, + } +} + +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: "[ ...]", + 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: "[ ...]", + 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) +} diff --git a/pkg/cli/sign.go b/pkg/cli/sign.go new file mode 100644 index 0000000..4f430d2 --- /dev/null +++ b/pkg/cli/sign.go @@ -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: "", + 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) +} diff --git a/pkg/commit/commit.go b/pkg/commit/commit.go new file mode 100644 index 0000000..f7d4b79 --- /dev/null +++ b/pkg/commit/commit.go @@ -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") +} diff --git a/pkg/dmg/assets/assets.go b/pkg/dmg/assets/assets.go new file mode 100644 index 0000000..0b5b596 --- /dev/null +++ b/pkg/dmg/assets/assets.go @@ -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 +} diff --git a/pkg/dmg/assets/bg.afdesign b/pkg/dmg/assets/bg.afdesign new file mode 100644 index 0000000..88deb58 Binary files /dev/null and b/pkg/dmg/assets/bg.afdesign differ diff --git a/pkg/dmg/assets/bg.png b/pkg/dmg/assets/bg.png new file mode 100644 index 0000000..2bef62d Binary files /dev/null and b/pkg/dmg/assets/bg.png differ diff --git a/pkg/dmg/assets/bg.tif b/pkg/dmg/assets/bg.tif new file mode 100644 index 0000000..38347c1 Binary files /dev/null and b/pkg/dmg/assets/bg.tif differ diff --git a/pkg/dmg/assets/bg@2x.png b/pkg/dmg/assets/bg@2x.png new file mode 100644 index 0000000..2990df6 Binary files /dev/null and b/pkg/dmg/assets/bg@2x.png differ diff --git a/pkg/dmg/assets/vol.icns b/pkg/dmg/assets/vol.icns new file mode 100644 index 0000000..01265a2 Binary files /dev/null and b/pkg/dmg/assets/vol.icns differ diff --git a/pkg/dmg/dmg.go b/pkg/dmg/dmg.go new file mode 100644 index 0000000..8ea665f --- /dev/null +++ b/pkg/dmg/dmg.go @@ -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 +} diff --git a/pkg/dmgbuild/dmgbuild.go b/pkg/dmgbuild/dmgbuild.go new file mode 100644 index 0000000..57e72ef --- /dev/null +++ b/pkg/dmgbuild/dmgbuild.go @@ -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 +} diff --git a/pkg/dmgbuild/icon_view.go b/pkg/dmgbuild/icon_view.go new file mode 100644 index 0000000..931e4ed --- /dev/null +++ b/pkg/dmgbuild/icon_view.go @@ -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 +} diff --git a/pkg/dmgbuild/license.go b/pkg/dmgbuild/license.go new file mode 100644 index 0000000..92f0499 --- /dev/null +++ b/pkg/dmgbuild/license.go @@ -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", + } +} diff --git a/pkg/dmgbuild/list_view.go b/pkg/dmgbuild/list_view.go new file mode 100644 index 0000000..b3f5c01 --- /dev/null +++ b/pkg/dmgbuild/list_view.go @@ -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 +} diff --git a/pkg/dmgbuild/settings.go b/pkg/dmgbuild/settings.go new file mode 100644 index 0000000..5b8c899 --- /dev/null +++ b/pkg/dmgbuild/settings.go @@ -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" +} diff --git a/pkg/dmgbuild/settings_test.go b/pkg/dmgbuild/settings_test.go new file mode 100644 index 0000000..e3ba25b --- /dev/null +++ b/pkg/dmgbuild/settings_test.go @@ -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) + }) + } +} diff --git a/pkg/dmgbuild/window.go b/pkg/dmgbuild/window.go new file mode 100644 index 0000000..a9e96c3 --- /dev/null +++ b/pkg/dmgbuild/window.go @@ -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 +} diff --git a/pkg/gh/gh.go b/pkg/gh/gh.go new file mode 100644 index 0000000..f614819 --- /dev/null +++ b/pkg/gh/gh.go @@ -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) +} diff --git a/pkg/notarize/notarize.go b/pkg/notarize/notarize.go new file mode 100644 index 0000000..eac1376 --- /dev/null +++ b/pkg/notarize/notarize.go @@ -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 := ¬arize.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) + } +} diff --git a/pkg/osinfo/osinfo.go b/pkg/osinfo/osinfo.go new file mode 100644 index 0000000..e8bca96 --- /dev/null +++ b/pkg/osinfo/osinfo.go @@ -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], ".") +} diff --git a/pkg/plan/create.go b/pkg/plan/create.go new file mode 100644 index 0000000..fc0eecc --- /dev/null +++ b/pkg/plan/create.go @@ -0,0 +1,125 @@ +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/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 + } + + releaseName := fmt.Sprintf( + "Emacs.%s.%s.%s", + commitInfo.DateString(), + commitInfo.ShortSHA(), + sanitizeString(opts.Ref), + ) + buildName := fmt.Sprintf( + "%s.%s.%s", + releaseName, + 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, + }, + Output: &Output{ + Directory: opts.OutputDir, + DiskImage: diskImage, + }, + } + + if opts.TestBuild != "" { + testName := sanitizeString(opts.TestBuild) + + plan.Build.Name += ".test." + testName + plan.Release.Title = "Test Builds" + plan.Release.Name = "test-builds" + if opts.TestBuildType == Draft { + plan.Release.Draft = true + } else { + plan.Release.Prerelease = 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, "-") +} diff --git a/pkg/plan/plan.go b/pkg/plan/plan.go new file mode 100644 index 0000000..e33181a --- /dev/null +++ b/pkg/plan/plan.go @@ -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"` +} diff --git a/pkg/release/check.go b/pkg/release/check.go new file mode 100644 index 0000000..c4c7a6d --- /dev/null +++ b/pkg/release/check.go @@ -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-"), + ) +} diff --git a/pkg/release/publish.go b/pkg/release/publish.go new file mode 100644 index 0000000..d96f675 --- /dev/null +++ b/pkg/release/publish.go @@ -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 +} diff --git a/pkg/repository/repository.go b/pkg/repository/repository.go new file mode 100644 index 0000000..98d5eeb --- /dev/null +++ b/pkg/repository/repository.go @@ -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 "" + } +} diff --git a/pkg/sign/emacs.go b/pkg/sign/emacs.go new file mode 100644 index 0000000..a5e0398 --- /dev/null +++ b/pkg/sign/emacs.go @@ -0,0 +1,159 @@ +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, +// based on expected paths they might be stored in. +func elnFiles(emacsApp string) ([]string, error) { + dirs := []string{ + // Current *.eln location. + filepath.Join(emacsApp, "Contents", "Resources", "native-lisp"), + // Legacy *.eln location. + filepath.Join(emacsApp, "Contents", "MacOS", "lib", "emacs"), + } + + var files []string + walkDirFunc := func(path string, _d fs.DirEntry, _err error) error { + if strings.HasSuffix(path, ".eln") { + files = append(files, path) + } + + return nil + } + + for _, dir := range dirs { + fi, err := os.Stat(dir) + if err != nil { + if os.IsNotExist(err) { + continue + } + + return nil, err + } + + if !fi.IsDir() { + continue + } + + err = filepath.WalkDir(dir, walkDirFunc) + if err != nil { + return nil, err + } + } + + return files, nil +} diff --git a/pkg/sign/entitlements.go b/pkg/sign/entitlements.go new file mode 100644 index 0000000..7941d20 --- /dev/null +++ b/pkg/sign/entitlements.go @@ -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 +} diff --git a/pkg/sign/entitlements.tpl b/pkg/sign/entitlements.tpl new file mode 100644 index 0000000..a0477ef --- /dev/null +++ b/pkg/sign/entitlements.tpl @@ -0,0 +1,9 @@ + + + + + {{- range . }} + {{ . }} + {{ end }} + + diff --git a/pkg/sign/entitlements_test.go b/pkg/sign/entitlements_test.go new file mode 100644 index 0000000..b0d5fae --- /dev/null +++ b/pkg/sign/entitlements_test.go @@ -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(` + + + + + + `, + ), + }, + { + name: "one", + entitlements: Entitlements{"com.apple.security.cs.allow-jit"}, + //nolint:lll + want: undent.String(` + + + + + com.apple.security.cs.allow-jit + + + `, + ), + }, + { + 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(` + + + + + com.apple.security.cs.allow-jit + + com.apple.security.network.client + + com.apple.security.cs.disable-library-validation + + com.apple.security.automation.apple-events + + + `, + ), + }, +} + +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\"", + ) + }) + } +} diff --git a/pkg/sign/files.go b/pkg/sign/files.go new file mode 100644 index 0000000..da2ca28 --- /dev/null +++ b/pkg/sign/files.go @@ -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() +} diff --git a/pkg/sign/options.go b/pkg/sign/options.go new file mode 100644 index 0000000..b2d4195 --- /dev/null +++ b/pkg/sign/options.go @@ -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 +} diff --git a/requirements-ci.txt b/requirements-ci.txt new file mode 100644 index 0000000..18ee57f --- /dev/null +++ b/requirements-ci.txt @@ -0,0 +1 @@ +dmgbuild