diff --git a/.github/workflows/publish-binaries.yml b/.github/workflows/publish-binaries.yml index fdb9e5e052..6817311955 100644 --- a/.github/workflows/publish-binaries.yml +++ b/.github/workflows/publish-binaries.yml @@ -1,9 +1,11 @@ name: Build @ironfish binaries on: - release: - types: - - published + push +# on: +# release: +# types: +# - published jobs: build: @@ -50,18 +52,29 @@ jobs: uses: actions/setup-node@v4 with: node-version: 18 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20.6' + + - name: Checkout repository + uses: actions/checkout@v4 - - name: npm init - run: npm init -y - - - name: install dependencies - run: npm install ironfish caxa@3.0.1 - - - name: caxa package - id: caxa + - name: create build.tar.gz for binary + id: build + run: | + mkdir build + cd build + npm init -y + npm install ironfish + tar -czf ../build.tar.gz -C . . + + - name: create binary + id: binary run: | - npx caxa --uncompression-message "Running the CLI for the first time may take a while, please wait..." --input . --output "${{ matrix.settings.system != 'windows' && 'ironfish' || 'ironfish.exe' }}" -- "{{caxa}}/node_modules/.bin/node" "--enable-source-maps" "{{caxa}}/node_modules/ironfish/bin/run" - echo "RELEASE_NAME=ironfish-${{ matrix.settings.system }}-${{ matrix.settings.arch }}-${{ github.event.release.tag_name }}.zip" + ls + go build -ldflags "-X 'main.Identifier=identifier' -X 'main.Command={{caxac}}/node --enable-source-maps {{caxac}}/node_modules/ironfish/bin/run' -X 'main.UncompressionMessage=Unpackaging ironfish application, this may take a minute when run for the first time.'" tools/build-binary.go - name: set paths id: set_paths diff --git a/tools/build-binary.go b/tools/build-binary.go new file mode 100644 index 0000000000..da74f1a3c5 --- /dev/null +++ b/tools/build-binary.go @@ -0,0 +1,237 @@ +package main + +import ( + "archive/tar" + "compress/gzip" + "context" + "embed" + "errors" + "fmt" + "io" + "log" + "os" + "os/exec" + "path" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" +) + +// When building this file, a build.tar.gz should be present and will be embedded in the binary + +//go:embed build.tar.gz +var data embed.FS + +var ( + Command string + UncompressionMessage string +) + +func main() { + + var applicationDirectory string + for extractionAttempt := 0; true; extractionAttempt++ { + lock := path.Join(os.TempDir(), "caxac/locks", strconv.Itoa(extractionAttempt)) + applicationDirectory = path.Join(os.TempDir(), "caxac/applications", strconv.Itoa(extractionAttempt)) + applicationDirectoryFileInfo, err := os.Stat(applicationDirectory) + if err != nil && !errors.Is(err, os.ErrNotExist) { + log.Fatalf("caxac stub: Failed to find information about the application directory: %v", err) + } + if err == nil && !applicationDirectoryFileInfo.IsDir() { + log.Fatalf("caxac stub: Path to application directory already exists and isn’t a directory: %v", err) + } + if err == nil && applicationDirectoryFileInfo.IsDir() { + lockFileInfo, err := os.Stat(lock) + if err != nil && !errors.Is(err, os.ErrNotExist) { + log.Fatalf("caxac stub: Failed to find information about the lock: %v", err) + } + if err == nil && !lockFileInfo.IsDir() { + log.Fatalf("caxac stub: Path to lock already exists and isn’t a directory: %v", err) + } + if err == nil && lockFileInfo.IsDir() { + // Application directory exists and lock exists as well, so a previous extraction wasn’t successful or an extraction is happening right now and hasn’t finished yet, in either case, start over with a fresh name. + continue + } + if err != nil && errors.Is(err, os.ErrNotExist) { + // Application directory exists and lock doesn’t exist, so a previous extraction was successful. Use the cached version of the application directory and don’t extract again. + break + } + } + if err != nil && errors.Is(err, os.ErrNotExist) { + ctx, cancelCtx := context.WithCancel(context.Background()) + if UncompressionMessage != "" { + fmt.Fprint(os.Stderr, UncompressionMessage) + go func() { + ticker := time.NewTicker(time.Second * 5) + defer ticker.Stop() + for { + select { + case <-ticker.C: + fmt.Fprint(os.Stderr, ".") + case <-ctx.Done(): + fmt.Fprintln(os.Stderr, "") + return + } + } + }() + } + + if err := os.MkdirAll(lock, 0755); err != nil { + log.Fatalf("caxac stub: Failed to create the lock directory: %v", err) + } + + embeddedDataReader, err := data.Open("build.tar.gz") + if err != nil { + log.Fatalf("Failed to open embedded data: %v", err) + } + defer embeddedDataReader.Close() + + if err := Untar(embeddedDataReader, applicationDirectory); err != nil { + log.Fatalf("caxac stub: Failed to uncompress embedded data: %v", err) + } + + os.Remove(lock) + + cancelCtx() + break + } + } + splitCommand := strings.Split(Command, " ") + expandedCommand := make([]string, len(splitCommand)) + applicationDirectoryPlaceholderRegexp := regexp.MustCompile(`\{\{\s*caxac\s*\}\}`) + for key, commandPart := range splitCommand { + expandedCommand[key] = applicationDirectoryPlaceholderRegexp.ReplaceAllLiteralString(commandPart, applicationDirectory) + } + + command := exec.Command(expandedCommand[0], append(expandedCommand[1:], os.Args[1:]...)...) + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + err := command.Run() + var exitError *exec.ExitError + if errors.As(err, &exitError) { + os.Exit(exitError.ExitCode()) + } else if err != nil { + log.Fatalf("caxac stub: Failed to run command: %v", err) + } +} + +// +// Adapted from https://github.com/leafac/caxa and https://github.com/golang/build/blob/db2c93053bcd6b944723c262828c90af91b0477a/internal/untar/untar.go and https://github.com/mholt/archiver/tree/v3.5.0 + +// Untar reads the gzip-compressed tar file from r and writes it into dir. +func Untar(r io.Reader, dir string) error { + return untar(r, dir) +} + +func untar(r io.Reader, dir string) (err error) { + t0 := time.Now() + nFiles := 0 + madeDir := map[string]bool{} + zr, err := gzip.NewReader(r) + if err != nil { + return fmt.Errorf("requires gzip-compressed body: %v", err) + } + tr := tar.NewReader(zr) + loggedChtimesError := false + for { + f, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("tar error: %v", err) + } + if !validRelPath(f.Name) { + return fmt.Errorf("tar contained invalid name error %q", f.Name) + } + rel := filepath.FromSlash(f.Name) + abs := filepath.Join(dir, rel) + + fi := f.FileInfo() + mode := fi.Mode() + switch { + case mode.IsRegular(): + // Make the directory. This is redundant because it should + // already be made by a directory entry in the tar + // beforehand. Thus, don't check for errors; the next + // write will fail with the same error. + dir := filepath.Dir(abs) + if !madeDir[dir] { + if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil { + return err + } + madeDir[dir] = true + } + wf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm()) + if err != nil { + return err + } + n, err := io.Copy(wf, tr) + if closeErr := wf.Close(); closeErr != nil && err == nil { + err = closeErr + } + if err != nil { + return fmt.Errorf("error writing to %s: %v", abs, err) + } + if n != f.Size { + return fmt.Errorf("only wrote %d bytes to %s; expected %d", n, abs, f.Size) + } + modTime := f.ModTime + if modTime.After(t0) { + // Clamp modtimes at system time. See + // golang.org/issue/19062 when clock on + // buildlet was behind the gitmirror server + // doing the git-archive. + modTime = t0 + } + if !modTime.IsZero() { + if err := os.Chtimes(abs, modTime, modTime); err != nil && !loggedChtimesError { + // benign error. Gerrit doesn't even set the + // modtime in these, and we don't end up relying + // on it anywhere (the gomote push command relies + // on digests only), so this is a little pointless + // for now. + // log.Printf("error changing modtime: %v (further Chtimes errors suppressed)", err) + loggedChtimesError = true // once is enough + } + } + nFiles++ + case mode.IsDir(): + if err := os.MkdirAll(abs, 0755); err != nil { + return err + } + madeDir[abs] = true + case f.Typeflag == tar.TypeSymlink: + // leafac: Added by me to support symbolic links. Adapted from https://github.com/mholt/archiver/blob/v3.5.0/tar.go#L254-L276 and https://github.com/mholt/archiver/blob/v3.5.0/archiver.go#L313-L332 + err := os.MkdirAll(filepath.Dir(abs), 0755) + if err != nil { + return fmt.Errorf("%s: making directory for file: %v", abs, err) + } + _, err = os.Lstat(abs) + if err == nil { + err = os.Remove(abs) + if err != nil { + return fmt.Errorf("%s: failed to unlink: %+v", abs, err) + } + } + + err = os.Symlink(f.Linkname, abs) + if err != nil { + return fmt.Errorf("%s: making symbolic link for: %v", abs, err) + } + default: + return fmt.Errorf("tar file entry %s contained unsupported file type %v", f.Name, mode) + } + } + return nil +} + +func validRelPath(p string) bool { + if p == "" || strings.Contains(p, `\`) || strings.HasPrefix(p, "/") || strings.Contains(p, "../") { + return false + } + return true +}