aboutsummaryrefslogtreecommitdiff
path: root/go/tools/builders/pack.go
blob: ddbb1930b3c7c0f31eb0072a0e1510e51c5986a2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
// 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"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
)

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
}