mirror of
https://github.com/jimeh/build-emacs-for-macos.git
synced 2026-02-19 13:06:38 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
6d21d1bef4
|
|||
|
99aa76b398
|
|||
|
b60ca528f8
|
|||
|
23b8236e0a
|
|||
|
56d0364099
|
|||
|
6af597b427
|
|||
|
a331457e89
|
|||
| a4171555f5 | |||
|
adbcfc6fc4
|
28
CHANGELOG.md
28
CHANGELOG.md
@@ -2,6 +2,34 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
### [0.6.3](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.2...v0.6.3) (2021-06-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **patches:** correctly set ref when loading a build plan YAML ([99aa76b](https://github.com/jimeh/build-emacs-for-macos/commit/99aa76b3985195c310a20bafa19a8c7a4c8558fd))
|
||||||
|
|
||||||
|
### [0.6.2](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.1...v0.6.2) (2021-06-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **native_comp:** patch Emacs.pdmp for customized native-lisp paths ([23b8236](https://github.com/jimeh/build-emacs-for-macos/commit/23b8236e0a66fb09810e8422bedf02f7192a53e4))
|
||||||
|
|
||||||
|
### [0.6.1](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.0...v0.6.1) (2021-06-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **cask:** add missing --force flag to cask update command ([6af597b](https://github.com/jimeh/build-emacs-for-macos/commit/6af597b4271341f9796c3d9c356de9918e0f6f85))
|
||||||
|
|
||||||
|
## [0.6.0](https://github.com/jimeh/build-emacs-for-macos/compare/v0.5.2...v0.6.0) (2021-06-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **cask:** add cask update command to manage cask formula ([adbcfc6](https://github.com/jimeh/build-emacs-for-macos/commit/adbcfc6fc433fcc99b10dc5ccb51ba458333fa9c))
|
||||||
|
|
||||||
### [0.5.2](https://github.com/jimeh/build-emacs-for-macos/compare/v0.5.1...v0.5.2) (2021-06-27)
|
### [0.5.2](https://github.com/jimeh/build-emacs-for-macos/compare/v0.5.1...v0.5.2) (2021-06-27)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -117,9 +117,10 @@ class Build
|
|||||||
def load_plan(filename)
|
def load_plan(filename)
|
||||||
plan = YAML.safe_load(File.read(filename), [:Time])
|
plan = YAML.safe_load(File.read(filename), [:Time])
|
||||||
|
|
||||||
|
@ref = plan.dig('source', 'ref')
|
||||||
@meta = {
|
@meta = {
|
||||||
sha: plan.dig('source', 'commit', 'sha'),
|
sha: plan.dig('source', 'commit', 'sha'),
|
||||||
ref: plan.dig('source', 'ref'),
|
ref: @ref,
|
||||||
date: plan.dig('source', 'commit', 'date')
|
date: plan.dig('source', 'commit', 'date')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,6 +455,13 @@ class Build
|
|||||||
parent = File.dirname(parent)
|
parent = File.dirname(parent)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
eln_parts = eln_dir.match(
|
||||||
|
%r{/(\d+\.\d+\.\d+)/native-lisp/(\d+\.\d+\.\d+-\w+)(?:/.+)?$}i
|
||||||
|
)
|
||||||
|
if eln_parts
|
||||||
|
patch_dump_native_lisp_paths(app, eln_parts[1], eln_parts[2])
|
||||||
|
end
|
||||||
|
|
||||||
# Find native-lisp directory again after it has been renamed.
|
# Find native-lisp directory again after it has been renamed.
|
||||||
source = Dir['MacOS/libexec/emacs/**/eln-cache',
|
source = Dir['MacOS/libexec/emacs/**/eln-cache',
|
||||||
'MacOS/lib/emacs/**/native-lisp'].first
|
'MacOS/lib/emacs/**/native-lisp'].first
|
||||||
@@ -468,6 +476,29 @@ class Build
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def patch_dump_native_lisp_paths(app, emacs_version, eln_version)
|
||||||
|
sanitized_emacs_version = emacs_version.gsub('.', '-')
|
||||||
|
sanitized_eln_version = eln_version.gsub('.', '-')
|
||||||
|
|
||||||
|
contents_dir = File.join(app, 'Contents')
|
||||||
|
FileUtils.cd(contents_dir) do
|
||||||
|
filename = Dir['MacOS/Emacs.pdmp', 'MacOS/libexec/Emacs.pdmp'].first
|
||||||
|
err "no Emacs.pdmp file found in #{app}" unless filename
|
||||||
|
info 'patching Emacs.pdmp to point at new native-lisp paths'
|
||||||
|
|
||||||
|
content = File.read(filename, mode: 'rb').gsub(
|
||||||
|
"lib/emacs/#{emacs_version}/native-lisp/#{eln_version}/",
|
||||||
|
"lib/emacs/#{sanitized_emacs_version}/" \
|
||||||
|
"native-lisp/#{sanitized_eln_version}/"
|
||||||
|
).gsub(
|
||||||
|
"../native-lisp/#{eln_version}/",
|
||||||
|
"../native-lisp/#{sanitized_eln_version}/"
|
||||||
|
)
|
||||||
|
|
||||||
|
File.open(filename, 'w') { |f| f.write(content) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def add_cli_helper(app)
|
def add_cli_helper(app)
|
||||||
source = File.join(__dir__, 'helper', 'emacs-cli.bash')
|
source = File.join(__dir__, 'helper', 'emacs-cli.bash')
|
||||||
target = File.join(app, 'Contents', 'MacOS', 'bin', 'emacs')
|
target = File.join(app, 'Contents', 'MacOS', 'bin', 'emacs')
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -13,6 +13,7 @@ require (
|
|||||||
github.com/hashicorp/go-hclog v0.16.1
|
github.com/hashicorp/go-hclog v0.16.1
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
|
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
|
||||||
|
github.com/hexops/gotextdiff v1.0.3
|
||||||
github.com/jimeh/undent v1.1.0
|
github.com/jimeh/undent v1.1.0
|
||||||
github.com/mattn/go-isatty v0.0.13 // indirect
|
github.com/mattn/go-isatty v0.0.13 // indirect
|
||||||
github.com/mitchellh/gon v0.2.3
|
github.com/mitchellh/gon v0.2.3
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -136,6 +136,8 @@ github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER
|
|||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
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/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/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/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/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 h1:Cge7P4Ws6buy0SVuHBluY/aOKdFuJUMzoJswfAHZ4zE=
|
||||||
|
|||||||
13
pkg/cask/live_check.go
Normal file
13
pkg/cask/live_check.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package cask
|
||||||
|
|
||||||
|
type LiveCheck struct {
|
||||||
|
Cask string `json:"cask"`
|
||||||
|
Version LiveCheckVersion `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LiveCheckVersion struct {
|
||||||
|
Current string `json:"current"`
|
||||||
|
Latest string `json:"latest"`
|
||||||
|
Outdated bool `json:"outdated"`
|
||||||
|
NewerThanUpstream bool `json:"newer_than_upstream"`
|
||||||
|
}
|
||||||
60
pkg/cask/release_info.go
Normal file
60
pkg/cask/release_info.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package cask
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReleaseInfo struct {
|
||||||
|
Name string
|
||||||
|
Version string
|
||||||
|
Assets map[string]*ReleaseAsset
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ReleaseInfo) Asset(nameMatch string) *ReleaseAsset {
|
||||||
|
if a, ok := s.Assets[nameMatch]; ok {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dirty and inefficient way to ensure assets are searched in a predictable
|
||||||
|
// order.
|
||||||
|
var assets []*ReleaseAsset
|
||||||
|
for _, a := range s.Assets {
|
||||||
|
assets = append(assets, a)
|
||||||
|
}
|
||||||
|
sort.SliceStable(assets, func(i, j int) bool {
|
||||||
|
return assets[i].Filename < assets[j].Filename
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, a := range assets {
|
||||||
|
if strings.Contains(a.Filename, nameMatch) {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ReleaseInfo) DownloadURL(nameMatch string) string {
|
||||||
|
a := s.Asset(nameMatch)
|
||||||
|
if a == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.DownloadURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ReleaseInfo) SHA256(nameMatch string) string {
|
||||||
|
a := s.Asset(nameMatch)
|
||||||
|
if a == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.SHA256
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReleaseAsset struct {
|
||||||
|
Filename string
|
||||||
|
DownloadURL string
|
||||||
|
SHA256 string
|
||||||
|
}
|
||||||
487
pkg/cask/update.go
Normal file
487
pkg/cask/update.go
Normal file
@@ -0,0 +1,487 @@
|
|||||||
|
package cask
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-github/v35/github"
|
||||||
|
"github.com/hashicorp/go-hclog"
|
||||||
|
"github.com/hexops/gotextdiff"
|
||||||
|
"github.com/hexops/gotextdiff/myers"
|
||||||
|
"github.com/hexops/gotextdiff/span"
|
||||||
|
"github.com/jimeh/build-emacs-for-macos/pkg/gh"
|
||||||
|
"github.com/jimeh/build-emacs-for-macos/pkg/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error vars
|
||||||
|
var (
|
||||||
|
Err = errors.New("cask")
|
||||||
|
ErrReleaseNotFound = fmt.Errorf("%w: release not found", Err)
|
||||||
|
|
||||||
|
ErrFailedSHA256Parse = fmt.Errorf(
|
||||||
|
"%w: failed to parse SHA256 from asset", Err,
|
||||||
|
)
|
||||||
|
ErrFailedSHA256Download = fmt.Errorf(
|
||||||
|
"%w: failed to download SHA256 asset", Err,
|
||||||
|
)
|
||||||
|
ErrNoTapOrOutput = fmt.Errorf(
|
||||||
|
"%w: no tap repository or output directory specified", Err,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpdateOptions struct {
|
||||||
|
// BuildsRepo is the GitHub repository containing binary releases.
|
||||||
|
BuildsRepo *repository.Repository
|
||||||
|
|
||||||
|
// TapRepo is the GitHub repository to update the cask formula in.
|
||||||
|
TapRepo *repository.Repository
|
||||||
|
|
||||||
|
// Ref is the git ref to apply cask formula updates on top of. Default
|
||||||
|
// branch will be used if empty.
|
||||||
|
Ref string
|
||||||
|
|
||||||
|
// OutputDir specifies a directory to write cask files to. When set, tap
|
||||||
|
// repository is ignored and no changes will be committed directly against
|
||||||
|
// any specified tap repository.
|
||||||
|
OutputDir string
|
||||||
|
|
||||||
|
// Force update will ignore the outdated live check flag, and process all
|
||||||
|
// casks regardless. But it will only update the cask in question if the
|
||||||
|
// resulting output cask formula is different.
|
||||||
|
Force bool
|
||||||
|
|
||||||
|
// TemplatesDir is the directory where cask formula templates are located.
|
||||||
|
TemplatesDir string
|
||||||
|
|
||||||
|
LiveChecks []*LiveCheck
|
||||||
|
|
||||||
|
GithubToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Updater struct {
|
||||||
|
BuildsRepo *repository.Repository
|
||||||
|
TapRepo *repository.Repository
|
||||||
|
Ref string
|
||||||
|
OutputDir string
|
||||||
|
TemplatesDir string
|
||||||
|
|
||||||
|
logger hclog.Logger
|
||||||
|
gh *github.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func Update(ctx context.Context, opts *UpdateOptions) error {
|
||||||
|
updater := &Updater{
|
||||||
|
BuildsRepo: opts.BuildsRepo,
|
||||||
|
TapRepo: opts.TapRepo,
|
||||||
|
Ref: opts.Ref,
|
||||||
|
OutputDir: opts.OutputDir,
|
||||||
|
TemplatesDir: opts.TemplatesDir,
|
||||||
|
logger: hclog.FromContext(ctx).Named("cask"),
|
||||||
|
gh: gh.New(ctx, opts.GithubToken),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, chk := range opts.LiveChecks {
|
||||||
|
err := updater.Update(ctx, chk, opts.Force)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Updater) Update(
|
||||||
|
ctx context.Context,
|
||||||
|
chk *LiveCheck,
|
||||||
|
force bool,
|
||||||
|
) error {
|
||||||
|
if s.TapRepo == nil && s.OutputDir == "" {
|
||||||
|
return ErrNoTapOrOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
if !force && !chk.Version.Outdated {
|
||||||
|
s.logger.Info("skipping", "cask", chk.Cask, "reason", "up to date")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
newCaskContent, err := s.renderCask(ctx, chk)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
caskFile := chk.Cask + ".rb"
|
||||||
|
|
||||||
|
if s.OutputDir != "" {
|
||||||
|
_, err = s.putFile(
|
||||||
|
ctx, chk, filepath.Join(s.OutputDir, caskFile), newCaskContent,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.putRepoFile(
|
||||||
|
ctx, s.TapRepo, s.Ref, chk,
|
||||||
|
filepath.Join("Casks", caskFile), newCaskContent,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Updater) putFile(
|
||||||
|
ctx context.Context,
|
||||||
|
chk *LiveCheck,
|
||||||
|
filename string,
|
||||||
|
content []byte,
|
||||||
|
) (bool, error) {
|
||||||
|
parent := filepath.Dir(filename)
|
||||||
|
s.logger.Info("processing formula update",
|
||||||
|
"output-directory", parent, "cask", chk.Cask, "file", filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
err := os.MkdirAll(parent, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
existingContent, err := os.ReadFile(filename)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
infoMsg := "creating formula"
|
||||||
|
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
infoMsg = "updating formula"
|
||||||
|
if bytes.Equal(existingContent, content) {
|
||||||
|
s.logger.Info(
|
||||||
|
"skip update: no change to cask formula content",
|
||||||
|
"cask", chk.Cask, "file", filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
s.logger.Debug(
|
||||||
|
"formula content",
|
||||||
|
"file", filename, "content", string(content),
|
||||||
|
)
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
existing := string(existingContent)
|
||||||
|
edits := myers.ComputeEdits(
|
||||||
|
span.URIFromPath(filename), existing, string(content),
|
||||||
|
)
|
||||||
|
diff := fmt.Sprint(gotextdiff.ToUnified(
|
||||||
|
filename, filename, existing, edits,
|
||||||
|
))
|
||||||
|
|
||||||
|
s.logger.Info(
|
||||||
|
infoMsg,
|
||||||
|
"cask", chk.Cask, "version", chk.Version.Latest, "file", filename,
|
||||||
|
"diff", diff,
|
||||||
|
)
|
||||||
|
|
||||||
|
s.logger.Debug(
|
||||||
|
"formula content",
|
||||||
|
"file", filename, "content", string(content),
|
||||||
|
)
|
||||||
|
|
||||||
|
err = os.WriteFile(filename, content, 0o644) //nolint:gosec
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Updater) putRepoFile(
|
||||||
|
ctx context.Context,
|
||||||
|
repo *repository.Repository,
|
||||||
|
ref string,
|
||||||
|
chk *LiveCheck,
|
||||||
|
filename string,
|
||||||
|
content []byte,
|
||||||
|
) (bool, error) {
|
||||||
|
s.logger.Info("processing formula update",
|
||||||
|
"tap-repo", repo.Source, "cask", chk.Cask, "file", filename,
|
||||||
|
)
|
||||||
|
repoContent, _, resp, err := s.gh.Repositories.GetContents(
|
||||||
|
ctx, repo.Owner(), repo.Name(), filename,
|
||||||
|
&github.RepositoryContentGetOptions{Ref: ref},
|
||||||
|
)
|
||||||
|
if err != nil && resp.StatusCode != http.StatusNotFound {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
|
err := s.createRepoFile(ctx, repo, chk, filename, content)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err := s.updateRepoFile(
|
||||||
|
ctx, repo, repoContent, chk, filename, content,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Updater) createRepoFile(
|
||||||
|
ctx context.Context,
|
||||||
|
repo *repository.Repository,
|
||||||
|
chk *LiveCheck,
|
||||||
|
filename string,
|
||||||
|
content []byte,
|
||||||
|
) error {
|
||||||
|
commitMsg := fmt.Sprintf(
|
||||||
|
"feat(cask): create %s with version %s",
|
||||||
|
chk.Cask, chk.Version.Latest,
|
||||||
|
)
|
||||||
|
|
||||||
|
edits := myers.ComputeEdits(
|
||||||
|
span.URIFromPath(filename), "", string(content),
|
||||||
|
)
|
||||||
|
diff := fmt.Sprint(gotextdiff.ToUnified(filename, filename, "", edits))
|
||||||
|
|
||||||
|
s.logger.Info(
|
||||||
|
"creating formula",
|
||||||
|
"cask", chk.Cask, "version", chk.Version.Latest, "file", filename,
|
||||||
|
"diff", diff,
|
||||||
|
)
|
||||||
|
s.logger.Debug(
|
||||||
|
"formula content",
|
||||||
|
"file", filename, "content", string(content),
|
||||||
|
)
|
||||||
|
contResp, _, err := s.gh.Repositories.CreateFile(
|
||||||
|
ctx, repo.Owner(), repo.Name(), filename,
|
||||||
|
&github.RepositoryContentFileOptions{
|
||||||
|
Message: &commitMsg,
|
||||||
|
Content: content,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info(
|
||||||
|
"new commit created",
|
||||||
|
"commit", contResp.GetSHA(), "message", contResp.GetMessage(),
|
||||||
|
"url", contResp.Commit.GetHTMLURL(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Updater) updateRepoFile(
|
||||||
|
ctx context.Context,
|
||||||
|
repo *repository.Repository,
|
||||||
|
repoContent *github.RepositoryContent,
|
||||||
|
chk *LiveCheck,
|
||||||
|
filename string,
|
||||||
|
content []byte,
|
||||||
|
) (bool, error) {
|
||||||
|
existingContent, err := repoContent.GetContent()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if existingContent == string(content) {
|
||||||
|
s.logger.Info(
|
||||||
|
"skip update: no change to formula content",
|
||||||
|
"cask", chk.Cask, "file", filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sha := repoContent.GetSHA()
|
||||||
|
|
||||||
|
commitMsg := fmt.Sprintf(
|
||||||
|
"feat(cask): update %s to version %s",
|
||||||
|
chk.Cask, chk.Version.Latest,
|
||||||
|
)
|
||||||
|
|
||||||
|
edits := myers.ComputeEdits(
|
||||||
|
span.URIFromPath(filename), existingContent, string(content),
|
||||||
|
)
|
||||||
|
diff := fmt.Sprint(gotextdiff.ToUnified(
|
||||||
|
filename, filename, existingContent, edits,
|
||||||
|
))
|
||||||
|
|
||||||
|
s.logger.Info(
|
||||||
|
"updating formula",
|
||||||
|
"cask", chk.Cask, "version", chk.Version.Latest, "file", filename,
|
||||||
|
"diff", diff,
|
||||||
|
)
|
||||||
|
s.logger.Debug(
|
||||||
|
"formula content",
|
||||||
|
"file", filename, "content", string(content),
|
||||||
|
)
|
||||||
|
|
||||||
|
contResp, _, err := s.gh.Repositories.CreateFile(
|
||||||
|
ctx, repo.Owner(), repo.Name(), filename,
|
||||||
|
&github.RepositoryContentFileOptions{
|
||||||
|
Message: &commitMsg,
|
||||||
|
Content: content,
|
||||||
|
SHA: &sha,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info(
|
||||||
|
"new commit created",
|
||||||
|
"commit", contResp.GetSHA(), "message", contResp.GetMessage(),
|
||||||
|
"url", contResp.Commit.GetHTMLURL(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Updater) renderCask(
|
||||||
|
ctx context.Context,
|
||||||
|
chk *LiveCheck,
|
||||||
|
) ([]byte, error) {
|
||||||
|
releaseName := "Emacs." + chk.Version.Latest
|
||||||
|
|
||||||
|
s.logger.Info("fetching release details", "release", releaseName)
|
||||||
|
release, resp, err := s.gh.Repositories.GetReleaseByTag(
|
||||||
|
ctx, s.BuildsRepo.Owner(), s.BuildsRepo.Name(), releaseName,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if release == nil || resp.StatusCode == http.StatusNotFound {
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrReleaseNotFound, releaseName)
|
||||||
|
}
|
||||||
|
|
||||||
|
info := &ReleaseInfo{
|
||||||
|
Name: release.GetName(),
|
||||||
|
Version: chk.Version.Latest,
|
||||||
|
Assets: map[string]*ReleaseAsset{},
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("processing release assets")
|
||||||
|
for _, asset := range release.Assets {
|
||||||
|
filename := asset.GetName()
|
||||||
|
s.logger.Debug("processing asset", "filename", filename)
|
||||||
|
|
||||||
|
if strings.HasSuffix(filename, ".sha256") {
|
||||||
|
filename = strings.TrimSuffix(filename, ".sha256")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := info.Assets[filename]; !ok {
|
||||||
|
info.Assets[filename] = &ReleaseAsset{
|
||||||
|
Filename: filename,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(asset.GetName(), ".sha256") {
|
||||||
|
s.logger.Debug("downloading *.sha256 asset to extract SHA256 value")
|
||||||
|
r, err2 := s.downloadAssetContent(ctx, asset)
|
||||||
|
if err2 != nil {
|
||||||
|
return nil, err2
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
content := make([]byte, 64)
|
||||||
|
n, err2 := io.ReadAtLeast(r, content, 64)
|
||||||
|
if err2 != nil {
|
||||||
|
return nil, err2
|
||||||
|
}
|
||||||
|
if n < 64 {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"%w: %s", ErrFailedSHA256Parse, asset.GetName(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
sha := string(content)[0:64]
|
||||||
|
if sha == "" {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"%w: %s", ErrFailedSHA256Parse, asset.GetName(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
info.Assets[filename].SHA256 = sha
|
||||||
|
} else {
|
||||||
|
info.Assets[filename].DownloadURL = asset.GetBrowserDownloadURL()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templateFile := filepath.Join(s.TemplatesDir, chk.Cask+".rb.tpl")
|
||||||
|
tplContent, err := os.ReadFile(templateFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tpl, err := template.New(chk.Cask).Parse(string(tplContent))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = tpl.Execute(&buf, info)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Updater) downloadAssetContent(
|
||||||
|
ctx context.Context,
|
||||||
|
asset *github.ReleaseAsset,
|
||||||
|
) (io.ReadCloser, error) {
|
||||||
|
httpClient := &http.Client{Timeout: 60 * time.Second}
|
||||||
|
|
||||||
|
r, downloadURL, err := s.gh.Repositories.DownloadReleaseAsset(
|
||||||
|
ctx, s.BuildsRepo.Owner(), s.BuildsRepo.Name(),
|
||||||
|
asset.GetID(), httpClient,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if r == nil && downloadURL != "" {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:bodyclose
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r = resp.Body
|
||||||
|
}
|
||||||
|
|
||||||
|
if r == nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"%s: %s", ErrFailedSHA256Download, asset.GetName(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
158
pkg/cli/cask.go
Normal file
158
pkg/cli/cask.go
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/jimeh/build-emacs-for-macos/pkg/cask"
|
||||||
|
"github.com/jimeh/build-emacs-for-macos/pkg/repository"
|
||||||
|
cli2 "github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type caskOptions struct {
|
||||||
|
BuildsRepo *repository.Repository
|
||||||
|
GithubToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
func caskCmd() *cli2.Command {
|
||||||
|
tokenDefaultText := ""
|
||||||
|
if len(os.Getenv("GITHUB_TOKEN")) > 0 {
|
||||||
|
tokenDefaultText = "***"
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cli2.Command{
|
||||||
|
Name: "cask",
|
||||||
|
Usage: "manage Homebrew Cask formula",
|
||||||
|
Flags: []cli2.Flag{
|
||||||
|
&cli2.StringFlag{
|
||||||
|
Name: "builds-repository",
|
||||||
|
Aliases: []string{"builds-repo", "b"},
|
||||||
|
Usage: "owner/name of GitHub repo for containing builds",
|
||||||
|
EnvVars: []string{"EMACS_BUILDS_REPOSITORY"},
|
||||||
|
Value: "jimeh/emacs-builds",
|
||||||
|
},
|
||||||
|
&cli2.StringFlag{
|
||||||
|
Name: "github-token",
|
||||||
|
Usage: "GitHub API Token",
|
||||||
|
EnvVars: []string{"GITHUB_TOKEN"},
|
||||||
|
DefaultText: tokenDefaultText,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Subcommands: []*cli2.Command{
|
||||||
|
caskUpdateCmd(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func caskActionWrapper(
|
||||||
|
f func(*cli2.Context, *Options, *caskOptions) error,
|
||||||
|
) func(*cli2.Context) error {
|
||||||
|
return actionWrapper(func(c *cli2.Context, opts *Options) error {
|
||||||
|
rOpts := &caskOptions{
|
||||||
|
GithubToken: c.String("github-token"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if r := c.String("builds-repository"); r != "" {
|
||||||
|
var err error
|
||||||
|
rOpts.BuildsRepo, err = repository.NewGitHub(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return f(c, opts, rOpts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func caskUpdateCmd() *cli2.Command {
|
||||||
|
return &cli2.Command{
|
||||||
|
Name: "update",
|
||||||
|
Usage: "update casks based on brew livecheck result in JSON format",
|
||||||
|
ArgsUsage: "<livecheck.json>",
|
||||||
|
Flags: []cli2.Flag{
|
||||||
|
&cli2.StringFlag{
|
||||||
|
Name: "ref",
|
||||||
|
Usage: "git ref to create/update casks on top of in the " +
|
||||||
|
"tap repository",
|
||||||
|
EnvVars: []string{"GITHUB_REF"},
|
||||||
|
},
|
||||||
|
&cli2.StringFlag{
|
||||||
|
Name: "output",
|
||||||
|
Aliases: []string{"o"},
|
||||||
|
Usage: "directory to write cask files to",
|
||||||
|
},
|
||||||
|
&cli2.StringFlag{
|
||||||
|
Name: "tap-repository",
|
||||||
|
Aliases: []string{"tap"},
|
||||||
|
Usage: "owner/name of GitHub repo for Homebrew Tap to " +
|
||||||
|
"commit changes to if --output is not set",
|
||||||
|
EnvVars: []string{"GITHUB_REPOSITORY"},
|
||||||
|
},
|
||||||
|
&cli2.StringFlag{
|
||||||
|
Name: "templates-dir",
|
||||||
|
Aliases: []string{"t"},
|
||||||
|
Usage: "path to directory of cask templates",
|
||||||
|
EnvVars: []string{"CASK_TEMPLATE_DIR"},
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
&cli2.BoolFlag{
|
||||||
|
Name: "force",
|
||||||
|
Aliases: []string{"f"},
|
||||||
|
Usage: "force update file even if livecheck has it marked " +
|
||||||
|
"as not outdated (does not force update if formula " +
|
||||||
|
"content is unchanged)",
|
||||||
|
Value: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: caskActionWrapper(caskUpdateAction),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func caskUpdateAction(
|
||||||
|
c *cli2.Context,
|
||||||
|
opts *Options,
|
||||||
|
cOpts *caskOptions,
|
||||||
|
) error {
|
||||||
|
updateOpts := &cask.UpdateOptions{
|
||||||
|
BuildsRepo: cOpts.BuildsRepo,
|
||||||
|
GithubToken: cOpts.GithubToken,
|
||||||
|
Ref: c.String("ref"),
|
||||||
|
OutputDir: c.String("output"),
|
||||||
|
Force: c.Bool("force"),
|
||||||
|
TemplatesDir: c.String("templates-dir"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if r := c.String("tap-repository"); r != "" {
|
||||||
|
var err error
|
||||||
|
updateOpts.TapRepo, err = repository.NewGitHub(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
arg := c.Args().First()
|
||||||
|
if arg == "" {
|
||||||
|
return errors.New("no livecheck argument given")
|
||||||
|
}
|
||||||
|
|
||||||
|
if arg == "-" {
|
||||||
|
err := json.NewDecoder(c.App.Reader).Decode(&updateOpts.LiveChecks)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
f, err := os.Open(arg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.NewDecoder(f).Decode(&updateOpts.LiveChecks)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cask.Update(c.Context, updateOpts)
|
||||||
|
}
|
||||||
@@ -49,6 +49,7 @@ func New(version, commit, date string) *CLI {
|
|||||||
notarizeCmd(),
|
notarizeCmd(),
|
||||||
packageCmd(),
|
packageCmd(),
|
||||||
releaseCmd(),
|
releaseCmd(),
|
||||||
|
caskCmd(),
|
||||||
{
|
{
|
||||||
Name: "version",
|
Name: "version",
|
||||||
Usage: "print the version",
|
Usage: "print the version",
|
||||||
|
|||||||
Reference in New Issue
Block a user