| // Copyright 2017 The Bazel Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package main |
| |
| import ( |
| "bufio" |
| "bytes" |
| "errors" |
| "flag" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "runtime" |
| "strconv" |
| "strings" |
| ) |
| |
| // pack copies an .a file and appends a list of .o files to the copy using |
| // go tool pack. It is invoked by the Go rules as an action. |
| // |
| // pack can also append .o files contained in a static library passed in |
| // with the -arc option. That archive may be in BSD or SysV / GNU format. |
| // pack has a primitive parser for these formats, since cmd/pack can't |
| // handle them, and ar may not be available (cpp.ar_executable is libtool |
| // on darwin). |
| func pack(args []string) error { |
| args, _, err := expandParamsFiles(args) |
| if err != nil { |
| return err |
| } |
| flags := flag.NewFlagSet("GoPack", flag.ExitOnError) |
| goenv := envFlags(flags) |
| inArchive := flags.String("in", "", "Path to input archive") |
| outArchive := flags.String("out", "", "Path to output archive") |
| objects := multiFlag{} |
| flags.Var(&objects, "obj", "Object to append (may be repeated)") |
| archives := multiFlag{} |
| flags.Var(&archives, "arc", "Archives to append") |
| if err := flags.Parse(args); err != nil { |
| return err |
| } |
| if err := goenv.checkFlags(); err != nil { |
| return err |
| } |
| |
| if err := copyFile(abs(*inArchive), abs(*outArchive)); err != nil { |
| return err |
| } |
| |
| dir, err := ioutil.TempDir("", "go-pack") |
| if err != nil { |
| return err |
| } |
| defer os.RemoveAll(dir) |
| |
| names := map[string]struct{}{} |
| for _, archive := range archives { |
| archiveObjects, err := extractFiles(archive, dir, names) |
| if err != nil { |
| return err |
| } |
| objects = append(objects, archiveObjects...) |
| } |
| |
| return appendFiles(goenv, abs(*outArchive), objects) |
| } |
| |
| func copyFile(inPath, outPath string) error { |
| inFile, err := os.Open(inPath) |
| if err != nil { |
| return err |
| } |
| defer inFile.Close() |
| outFile, err := os.OpenFile(outPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666) |
| if err != nil { |
| return err |
| } |
| defer outFile.Close() |
| _, err = io.Copy(outFile, inFile) |
| return err |
| } |
| |
| func linkFile(inPath, outPath string) error { |
| inPath, err := filepath.Abs(inPath) |
| if err != nil { |
| return err |
| } |
| return os.Symlink(inPath, outPath) |
| } |
| |
| func copyOrLinkFile(inPath, outPath string) error { |
| if runtime.GOOS == "windows" { |
| return copyFile(inPath, outPath) |
| } else { |
| return linkFile(inPath, outPath) |
| } |
| } |
| |
| const ( |
| // arHeader appears at the beginning of archives created by "ar" and |
| // "go tool pack" on all platforms. |
| arHeader = "!<arch>\n" |
| |
| // entryLength is the size in bytes of the metadata preceding each file |
| // in an archive. |
| entryLength = 60 |
| |
| // pkgDef is the name of the export data file within an archive |
| pkgDef = "__.PKGDEF" |
| |
| // nogoFact is the name of the nogo fact file |
| nogoFact = "nogo.out" |
| ) |
| |
| var zeroBytes = []byte("0 ") |
| |
| type bufioReaderWithCloser struct { |
| // bufio.Reader is needed to skip bytes in archives |
| *bufio.Reader |
| io.Closer |
| } |
| |
| func extractFiles(archive, dir string, names map[string]struct{}) (files []string, err error) { |
| rc, err := openArchive(archive) |
| if err != nil { |
| return nil, err |
| } |
| defer rc.Close() |
| |
| var nameData []byte |
| bufReader := rc.Reader |
| for { |
| name, size, err := readMetadata(bufReader, &nameData) |
| if err == io.EOF { |
| return files, nil |
| } |
| if err != nil { |
| return nil, err |
| } |
| if !isObjectFile(name) { |
| if err := skipFile(bufReader, size); err != nil { |
| return nil, err |
| } |
| continue |
| } |
| name, err = simpleName(name, names) |
| if err != nil { |
| return nil, err |
| } |
| name = filepath.Join(dir, name) |
| if err := extractFile(bufReader, name, size); err != nil { |
| return nil, err |
| } |
| files = append(files, name) |
| } |
| } |
| |
| func openArchive(archive string) (bufioReaderWithCloser, error) { |
| f, err := os.Open(archive) |
| if err != nil { |
| return bufioReaderWithCloser{}, err |
| } |
| r := bufio.NewReader(f) |
| header := make([]byte, len(arHeader)) |
| if _, err := io.ReadFull(r, header); err != nil || string(header) != arHeader { |
| f.Close() |
| return bufioReaderWithCloser{}, fmt.Errorf("%s: bad header", archive) |
| } |
| return bufioReaderWithCloser{r, f}, nil |
| } |
| |
| // readMetadata reads the relevant fields of an entry. Before calling, |
| // r must be positioned at the beginning of an entry. Afterward, r will |
| // be positioned at the beginning of the file data. io.EOF is returned if |
| // there are no more files in the archive. |
| // |
| // Both BSD and GNU / SysV naming conventions are supported. |
| func readMetadata(r *bufio.Reader, nameData *[]byte) (name string, size int64, err error) { |
| retry: |
| // Each file is preceded by a 60-byte header that contains its metadata. |
| // We only care about two fields, name and size. Other fields (mtime, |
| // owner, group, mode) are ignored because they don't affect compilation. |
| var entry [entryLength]byte |
| if _, err := io.ReadFull(r, entry[:]); err != nil { |
| return "", 0, err |
| } |
| |
| sizeField := strings.TrimSpace(string(entry[48:58])) |
| size, err = strconv.ParseInt(sizeField, 10, 64) |
| if err != nil { |
| return "", 0, err |
| } |
| |
| nameField := strings.TrimRight(string(entry[:16]), " ") |
| switch { |
| case strings.HasPrefix(nameField, "#1/"): |
| // BSD-style name. The number of bytes in the name is written here in |
| // ASCII, right-padded with spaces. The actual name is stored at the |
| // beginning of the file data, left-padded with NUL bytes. |
| nameField = nameField[len("#1/"):] |
| nameLen, err := strconv.ParseInt(nameField, 10, 64) |
| if err != nil { |
| return "", 0, err |
| } |
| nameBuf := make([]byte, nameLen) |
| if _, err := io.ReadFull(r, nameBuf); err != nil { |
| return "", 0, err |
| } |
| name = strings.TrimRight(string(nameBuf), "\x00") |
| size -= nameLen |
| |
| case nameField == "//": |
| // GNU / SysV-style name data. This is a fake file that contains names |
| // for files with long names. We read this into nameData, then read |
| // the next entry. |
| *nameData = make([]byte, size) |
| if _, err := io.ReadFull(r, *nameData); err != nil { |
| return "", 0, err |
| } |
| if size%2 != 0 { |
| // Files are aligned at 2-byte offsets. Discard the padding byte if the |
| // size was odd. |
| if _, err := r.ReadByte(); err != nil { |
| return "", 0, err |
| } |
| } |
| goto retry |
| |
| case nameField == "/": |
| // GNU / SysV-style symbol lookup table. Skip. |
| if err := skipFile(r, size); err != nil { |
| return "", 0, err |
| } |
| goto retry |
| |
| case strings.HasPrefix(nameField, "/"): |
| // GNU / SysV-style long file name. The number that follows the slash is |
| // an offset into the name data that should have been read earlier. |
| // The file name ends with a slash. |
| nameField = nameField[1:] |
| nameOffset, err := strconv.Atoi(nameField) |
| if err != nil { |
| return "", 0, err |
| } |
| if nameData == nil || nameOffset < 0 || nameOffset >= len(*nameData) { |
| return "", 0, fmt.Errorf("invalid name length: %d", nameOffset) |
| } |
| i := bytes.IndexByte((*nameData)[nameOffset:], '/') |
| if i < 0 { |
| return "", 0, errors.New("file name does not end with '/'") |
| } |
| name = string((*nameData)[nameOffset : nameOffset+i]) |
| |
| case strings.HasSuffix(nameField, "/"): |
| // GNU / SysV-style short file name. |
| name = nameField[:len(nameField)-1] |
| |
| default: |
| // Common format name. |
| name = nameField |
| } |
| |
| return name, size, err |
| } |
| |
| // extractFile reads size bytes from r and writes them to a new file, name. |
| func extractFile(r *bufio.Reader, name string, size int64) error { |
| w, err := os.Create(name) |
| if err != nil { |
| return err |
| } |
| defer w.Close() |
| _, err = io.CopyN(w, r, size) |
| if err != nil { |
| return err |
| } |
| if size%2 != 0 { |
| // Files are aligned at 2-byte offsets. Discard the padding byte if the |
| // size was odd. |
| if _, err := r.ReadByte(); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func skipFile(r *bufio.Reader, size int64) error { |
| if size%2 != 0 { |
| // Files are aligned at 2-byte offsets. Discard the padding byte if the |
| // size was odd. |
| size += 1 |
| } |
| _, err := r.Discard(int(size)) |
| return err |
| } |
| |
| func isObjectFile(name string) bool { |
| return strings.HasSuffix(name, ".o") |
| } |
| |
| // simpleName returns a file name which is at most 15 characters |
| // and doesn't conflict with other names. If it is not possible to choose |
| // such a name, simpleName will truncate the given name to 15 characters. |
| // The original file extension will be preserved. |
| func simpleName(name string, names map[string]struct{}) (string, error) { |
| if _, ok := names[name]; !ok && len(name) < 16 { |
| names[name] = struct{}{} |
| return name, nil |
| } |
| var stem, ext string |
| if i := strings.LastIndexByte(name, '.'); i < 0 { |
| stem = name |
| } else { |
| stem = strings.Replace(name[:i], ".", "_", -1) |
| ext = name[i:] |
| } |
| for n := 0; n < len(names)+1; n++ { |
| ns := strconv.Itoa(n) |
| stemLen := 15 - len(ext) - len(ns) |
| if stemLen < 0 { |
| break |
| } |
| if stemLen > len(stem) { |
| stemLen = len(stem) |
| } |
| candidate := stem[:stemLen] + ns + ext |
| if _, ok := names[candidate]; !ok { |
| names[candidate] = struct{}{} |
| return candidate, nil |
| } |
| } |
| return "", fmt.Errorf("cannot shorten file name: %q", name) |
| } |
| |
| func appendFiles(goenv *env, archive string, files []string) error { |
| archive = abs(archive) // required for long filenames on Windows. |
| |
| // Create an empty archive if one doesn't already exist. |
| // In Go 1.16, 'go tool pack r' reports an error if the archive doesn't exist. |
| // 'go tool pack c' copies export data in addition to creating the archive, |
| // so we don't want to use that directly. |
| _, err := os.Stat(archive) |
| if err != nil && !os.IsNotExist(err) { |
| return err |
| } |
| if os.IsNotExist(err) { |
| if err := ioutil.WriteFile(archive, []byte(arHeader), 0666); err != nil { |
| return err |
| } |
| } |
| |
| // Append files to the archive. |
| // TODO(jayconrod): copy cmd/internal/archive and use that instead of |
| // shelling out to cmd/pack. |
| args := goenv.goTool("pack", "r", archive) |
| args = append(args, files...) |
| return goenv.runCommand(args) |
| } |
| |
| type readWithCloser struct { |
| io.Reader |
| io.Closer |
| } |
| |
| func readFileInArchive(fileName, archive string) (io.ReadCloser, error) { |
| rc, err := openArchive(archive) |
| if err != nil { |
| return nil, err |
| } |
| var nameData []byte |
| bufReader := rc.Reader |
| for err == nil { |
| // avoid shadowing err in the loop it can be returned correctly in the end |
| var ( |
| name string |
| size int64 |
| ) |
| name, size, err = readMetadata(bufReader, &nameData) |
| if err != nil { |
| break |
| } |
| if name == fileName { |
| return readWithCloser{ |
| Reader: io.LimitReader(rc, size), |
| Closer: rc, |
| }, nil |
| } |
| err = skipFile(bufReader, size) |
| } |
| if err == io.EOF { |
| err = os.ErrNotExist |
| } |
| rc.Close() |
| return nil, err |
| } |
| |
| func extractFileFromArchive(archive, dir, name string) (err error) { |
| archiveReader, err := readFileInArchive(name, archive) |
| if err != nil { |
| return fmt.Errorf("error reading %s from %s: %v", name, archive, err) |
| } |
| defer func() { |
| e := archiveReader.Close() |
| if e != nil && err == nil { |
| err = fmt.Errorf("error closing %q: %v", archive, e) |
| } |
| }() |
| outPath := filepath.Join(dir, pkgDef) |
| outFile, err := os.Create(outPath) |
| if err != nil { |
| return fmt.Errorf("error creating %s: %v", outPath, err) |
| } |
| defer func() { |
| e := outFile.Close() |
| if e != nil && err == nil { |
| err = fmt.Errorf("error closing %q: %v", outPath, e) |
| } |
| }() |
| if size, err := io.Copy(outFile, archiveReader); err != nil { |
| return fmt.Errorf("error writing %s: %v", outPath, err) |
| } else if size == 0 { |
| return fmt.Errorf("%s is empty in %s", name, archive) |
| } |
| return err |
| } |