• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pact-foundation / pact-go / 12931992344

23 Jan 2025 03:03PM UTC coverage: 28.209% (-1.0%) from 29.225%
12931992344

push

github

web-flow
Merge pull request #454 from YOU54F/feat/linux-musl

feat: support linux musl

9 of 13 new or added lines in 1 file covered. (69.23%)

68 existing lines in 2 files now uncovered.

1802 of 6388 relevant lines covered (28.21%)

8.25 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

59.67
/installer/installer.go
1
// Package installer is responsible for finding, acquiring and addressing
2
// runtime dependencies for this package (e.g. Ruby standalone, Rust bindings etc.)
3
package installer
4

5
import (
6
        "compress/gzip"
7
        "fmt"
8
        "io"
9
        "log"
10
        "net/http"
11
        "os"
12
        "os/exec"
13
        "os/user"
14
        "path"
15
        "path/filepath"
16
        "runtime"
17
        "strings"
18

19
        goversion "github.com/hashicorp/go-version"
20
        "gopkg.in/yaml.v3"
21

22
        "crypto/md5"
23

24
        "github.com/spf13/afero"
25
)
26

27
// NativeLibPath returns the absolute path to the go package used to link to the native rust library
28
func NativeLibPath() string {
6✔
29
        _, file, _, ok := runtime.Caller(0)
6✔
30
        if !ok {
6✔
31
                return ""
×
32
        }
×
33
        pactRoot := filepath.Dir(filepath.Dir(file))
6✔
34
        return filepath.Join(pactRoot, "internal", "native")
6✔
35
}
36

37
// Installer is used to check the Pact Go installation is setup correctly, and can automatically install
38
// packages if required
39
type Installer struct {
40
        downloader downloader
41
        hasher     hasher
42
        config     configReadWriter
43
        os         string
44
        arch       string
45
        fs         afero.Fs
46
        libDir     string
47
        force      bool
48
}
49

50
type installerConfig func(*Installer) error
51

52
// NewInstaller creates a new initialised Installer
53
func NewInstaller(opts ...installerConfig) (*Installer, error) {
6✔
54
        i := &Installer{downloader: &defaultDownloader{}, fs: afero.NewOsFs(), hasher: &defaultHasher{}, config: &configuration{}}
6✔
55

6✔
56
        for _, opt := range opts {
12✔
57
                err := opt(i)
6✔
58
                if err != nil {
6✔
59
                        log.Println("[ERROR] failure when configuring installer:", err)
×
60
                        return nil, err
×
61
                }
×
62
        }
63

64
        if _, ok := supportedOSes[runtime.GOOS]; !ok {
6✔
65
                return nil, fmt.Errorf("%s is not a supported OS", runtime.GOOS)
×
66
        }
×
67
        i.os = supportedOSes[runtime.GOOS]
6✔
68

6✔
69
        if !strings.Contains(runtime.GOARCH, "64") {
6✔
70
                return nil, fmt.Errorf("%s is not a supported architecture, only 64 bit architectures are supported", runtime.GOARCH)
×
71
        }
×
72

73
        switch runtime.GOARCH {
6✔
74
        case "amd64":
4✔
75
                i.arch = x86_64
4✔
76
        case "arm64":
2✔
77
                i.arch = aarch64
2✔
78
        default:
×
79
                i.arch = x86_64
×
80
                log.Println("[WARN] amd64 architecture not detected, defaulting to x86_64. Behaviour may be undefined")
×
81
        }
82

83
        return i, nil
6✔
84
}
85

86
// SetLibDir overrides the default library dir
87
func (i *Installer) SetLibDir(dir string) {
×
88
        i.libDir = dir
×
89
}
×
90

91
// Force installs over the top
92
func (i *Installer) Force(force bool) {
×
93
        i.force = force
×
94
}
×
95

96
// CheckInstallation checks installation of all of the required libraries
97
// and downloads if they aren't present
98
func (i *Installer) CheckInstallation() error {
18✔
99

18✔
100
        // Check if files exist
18✔
101
        // --> Check if existing installed files
18✔
102
        if !i.force {
36✔
103
                if err := i.CheckPackageInstall(); err == nil {
24✔
104
                        return nil
6✔
105
                }
6✔
106
        }
107

108
        // Download dependencies
109
        if err := i.downloadDependencies(); err != nil {
12✔
110
                return err
×
111
        }
×
112

113
        // Install dependencies
114
        if err := i.installDependencies(); err != nil {
14✔
115
                return err
2✔
116
        }
2✔
117

118
        // Double check files landed correctly (can't execute 'version' call here,
119
        // because of dependency on the native libs we're trying to download!)
120
        if err := i.CheckPackageInstall(); err != nil {
14✔
121
                return fmt.Errorf("unable to verify downloaded/installed dependencies: %s", err)
4✔
122
        }
4✔
123

124
        return nil
6✔
125
}
126

127
func (i *Installer) getLibDir() string {
70✔
128
        if i.libDir != "" {
70✔
129
                return i.libDir
×
130
        }
×
131

132
        env := os.Getenv(downloadEnvVar)
70✔
133
        if env != "" {
140✔
134
                return env
70✔
135
        }
70✔
136

137
        return "/usr/local/lib"
×
138
}
139

140
// CheckPackageInstall discovers any existing packages, and checks installation of a given binary using semver-compatible checks
141
func (i *Installer) CheckPackageInstall() error {
28✔
142
        for pkg, info := range packages {
56✔
143

28✔
144
                dst, _ := i.getLibDstForPackage(pkg)
28✔
145

28✔
146
                if _, err := i.fs.Stat(dst); err != nil {
44✔
147
                        log.Println("[INFO] package", info.libName, "not found")
16✔
148
                        return err
16✔
149
                } else {
28✔
150
                        log.Println("[INFO] package", info.libName, "found")
12✔
151
                }
12✔
152

153
                lib, ok := i.config.readConfig().Libraries[pkg]
12✔
154
                if ok {
12✔
155
                        if err := checkVersion(info.libName, lib.Version, info.semverRange); err != nil {
×
156
                                return err
×
157
                        }
×
158
                        log.Println("[INFO] package", info.libName, "is correctly installed")
×
159
                } else {
12✔
160
                        log.Println("[INFO] no package metadata information was found, run `pact-go install -f` to correct")
12✔
161
                }
12✔
162

163
                // This will only be populated during test when the ffi is loaded, but will actually test the FFI itself
164
                // It is helpful because it will prevent issues where the FFI is manually updated without using the `pact-go install` command
165
                if len(LibRegistry) == 0 {
24✔
166
                        log.Println("[DEBUG] skip checking ffi version() call because FFI not loaded. This is expected when running the 'pact-go' command.")
12✔
167
                } else {
12✔
168
                        lib, ok := LibRegistry[pkg]
×
169

×
170
                        if ok {
×
171
                                log.Println("[INFO] checking version", lib.Version(), "for lib", info.libName, "within semver range", info.semverRange)
×
172
                                if err := checkVersion(info.libName, lib.Version(), info.semverRange); err != nil {
×
173
                                        return err
×
174
                                }
×
175
                        } else {
×
176
                                log.Println("[DEBUG] unable to determine current version of package", pkg, "in LibRegistry", LibRegistry)
×
177
                        }
×
178

179
                        // Correct the configuration to reduce drift
180
                        err := i.updateConfiguration(dst, pkg, info)
×
181
                        if err != nil {
×
182
                                return err
×
183
                        }
×
184
                }
185
        }
186

187
        return nil
12✔
188
}
189

190
// Download all dependencies, and update the pact-go configuration file
191
func (i *Installer) downloadDependencies() error {
18✔
192
        for pkg, pkgInfo := range packages {
36✔
193
                src, err := i.getDownloadURLForPackage(pkg)
18✔
194

18✔
195
                if err != nil {
18✔
196
                        return err
×
197
                }
×
198

199
                dst, err := i.getLibDstForPackage(pkg)
18✔
200

18✔
201
                if err != nil {
18✔
202
                        return err
×
203
                }
×
204

205
                err = i.downloader.download(src, dst)
18✔
206

18✔
207
                if err != nil {
18✔
208
                        return err
×
209
                }
×
210

211
                err = os.Chmod(dst, 0755)
18✔
212
                if err != nil {
22✔
213
                        log.Println("[WARN] unable to set permissions on file", dst, "due to error:", err)
4✔
214
                }
4✔
215

216
                err = i.updateConfiguration(dst, pkg, pkgInfo)
18✔
217

18✔
218
                if err != nil {
18✔
219
                        return err
×
220
                }
×
221
        }
222

223
        return nil
18✔
224
}
225

226
func (i *Installer) installDependencies() error {
12✔
227
        if i.os == macos {
24✔
228
                for pkg, info := range packages {
24✔
229
                        log.Println("[INFO] setting install_name on library", info.libName, "for macos")
12✔
230

12✔
231
                        dst, err := i.getLibDstForPackage(pkg)
12✔
232

12✔
233
                        if err != nil {
12✔
234
                                return err
×
235
                        }
×
236

237
                        err = setMacOSInstallName(dst)
12✔
238

12✔
239
                        if err != nil {
14✔
240
                                return err
2✔
241
                        }
2✔
242
                }
243
        }
244

245
        return nil
10✔
246
}
247

248
// returns src
249
func (i *Installer) getDownloadURLForPackage(pkg string) (string, error) {
48✔
250
        pkgInfo, ok := packages[pkg]
48✔
251
        if !ok {
48✔
252
                return "", fmt.Errorf("unable to find package details for package: %s", pkg)
×
253
        }
×
254

255
        if checkMusl() && i.os == linux {
48✔
NEW
256
                return fmt.Sprintf(downloadTemplate, pkg, pkgInfo.version, osToLibName[i.os], i.os, i.arch+"-musl", osToExtension[i.os]), nil
×
257
        } else {
48✔
258
                return fmt.Sprintf(downloadTemplate, pkg, pkgInfo.version, osToLibName[i.os], i.os, i.arch, osToExtension[i.os]), nil
48✔
259

48✔
260
        }
48✔
261

262
}
263

264
func (i *Installer) getLibDstForPackage(pkg string) (string, error) {
70✔
265
        _, ok := packages[pkg]
70✔
266
        if !ok {
70✔
267
                return "", fmt.Errorf("unable to find package details for package: %s", pkg)
×
268
        }
×
269

270
        return path.Join(i.getLibDir(), osToLibName[i.os]) + "." + osToExtension[i.os], nil
70✔
271
}
272

273
// Write the metadata to reduce drift
274
func (i *Installer) updateConfiguration(dst string, pkg string, info packageInfo) error {
18✔
275
        // Get hash of file
18✔
276
        fmt.Println(i.hasher)
18✔
277
        hash, err := i.hasher.hash(dst)
18✔
278
        if err != nil {
18✔
279
                return err
×
280
        }
×
281

282
        // Read metadata
283
        c := i.config.readConfig()
18✔
284

18✔
285
        // Update config
18✔
286
        c.Libraries[pkg] = packageMetadata{
18✔
287
                LibName: info.libName,
18✔
288
                Version: info.version,
18✔
289
                Hash:    hash,
18✔
290
        }
18✔
291

18✔
292
        // Write metadata
18✔
293
        return i.config.writeConfig(c)
18✔
294
}
295

296
var setMacOSInstallName = func(file string) error {
6✔
297
        cmd := exec.Command("install_name_tool", "-id", file, file)
6✔
298
        log.Println("[DEBUG] running command:", cmd)
6✔
299
        stdoutStderr, err := cmd.CombinedOutput()
6✔
300

6✔
301
        if err != nil {
8✔
302
                return fmt.Errorf("error setting install name on pact lib: %s", err)
2✔
303
        }
2✔
304

305
        log.Println("[DEBUG] output from command", stdoutStderr)
4✔
306

4✔
307
        return err
4✔
308
}
309

310
func checkVersion(lib, version, versionRange string) error {
×
311
        log.Println("[INFO] checking version", version, "of", lib, "against semver constraint", versionRange)
×
312

×
313
        v, err := goversion.NewVersion(version)
×
314
        if err != nil {
×
315
                return err
×
316
        }
×
317

318
        constraints, err := goversion.NewConstraint(versionRange)
×
319
        if err != nil {
×
320
                return err
×
321
        }
×
322

323
        if constraints.Check(v) {
×
324
                log.Println("[DEBUG]", v, "satisfies constraints", v, constraints)
×
325
                return nil
×
326
        }
×
327

328
        return fmt.Errorf("version %s of %s does not match constraint %s", version, lib, versionRange)
×
329
}
330

331
// checkMusl checks if the OS uses musl library instead of glibc
332
func checkMusl() bool {
60✔
333
        lddPath, err := exec.LookPath("ldd")
60✔
334
        if err != nil {
100✔
335
                return false
40✔
336
        }
40✔
337

338
        cmd := exec.Command(lddPath, "/bin/echo")
20✔
339
        out, err := cmd.CombinedOutput()
20✔
340

20✔
341
        if err != nil {
20✔
NEW
342
                return false
×
NEW
343
        }
×
344
        if strings.Contains(string(out), "musl") {
20✔
NEW
345
                return true
×
346
        }
×
347

348
        return false
20✔
349
}
350

351
// download template structure: "https://github.com/pact-foundation/pact-reference/releases/download/PACKAGE-vVERSION/LIBNAME-OS-ARCH.EXTENSION.gz"
352
var downloadTemplate = "https://github.com/pact-foundation/pact-reference/releases/download/%s-v%s/%s-%s-%s.%s.gz"
353

354
var supportedOSes = map[string]string{
355
        "darwin": macos,
356
        windows:  windows,
357
        linux:    linux,
358
}
359

360
var osToExtension = map[string]string{
361
        windows: "dll",
362
        linux:   "so",
363
        macos:   "dylib",
364
}
365

366
var osToLibName = map[string]string{
367
        windows: "pact_ffi",
368
        linux:   "libpact_ffi",
369
        macos:   "libpact_ffi",
370
}
371

372
type packageInfo struct {
373
        libName     string
374
        version     string
375
        semverRange string
376
}
377

378
const (
379
        FFIPackage     = "libpact_ffi"
380
        downloadEnvVar = "PACT_GO_LIB_DOWNLOAD_PATH"
381
        linux          = "linux"
382
        windows        = "windows"
383
        macos          = "macos"
384
        x86_64         = "x86_64"
385
        aarch64        = "aarch64"
386
)
387

388
var packages = map[string]packageInfo{
389
        FFIPackage: {
390
                libName:     "libpact_ffi",
391
                version:     "0.4.26",
392
                semverRange: ">= 0.4.0, < 1.0.0",
393
        },
394
}
395

396
type Versioner interface {
397
        Version() string
398
}
399

400
var LibRegistry = map[string]Versioner{}
401

402
type downloader interface {
403
        download(src string, dst string) error
404
}
405

406
type defaultDownloader struct{}
407

408
func (d *defaultDownloader) download(src string, dst string) error {
×
409
        log.Println("[INFO] downloading library from", src, "to", dst)
×
410

×
411
        baseDir := path.Dir(dst)
×
412
        if err := os.MkdirAll(baseDir, 0755); err != nil {
×
413
                return fmt.Errorf("failed to create %s; %w", baseDir, err)
×
414
        }
×
415

416
        f, err := os.Create(dst)
×
417
        if err != nil {
×
418
                return fmt.Errorf("failed to create output file; %w", err)
×
419
        }
×
420
        defer f.Close()
×
421

×
422
        resp, err := http.Get(src)
×
423
        if err != nil {
×
424
                return fmt.Errorf("failed http call to %s; %w", src, err)
×
425
        }
×
426
        defer resp.Body.Close()
×
427

×
428
        archive, err := gzip.NewReader(resp.Body)
×
429
        if err != nil {
×
430
                return fmt.Errorf("failed to create new gzip reader; %w", err)
×
431
        }
×
432

433
        _, err = io.Copy(f, archive)
×
434
        if err != nil {
×
435
                return fmt.Errorf("failed to copy archive to file; %w", err)
×
436
        }
×
437

438
        return nil
×
439
}
440

441
type packageMetadata struct {
442
        LibName string
443
        Version string
444
        Hash    string
445
}
446

447
type pactConfig struct {
448
        Libraries map[string]packageMetadata
449
}
450

451
type configReader interface {
452
        readConfig() pactConfig
453
}
454
type configWriter interface {
455
        writeConfig(pactConfig) error
456
}
457

458
type configReadWriter interface {
459
        configReader
460
        configWriter
461
}
462

463
type configuration struct{}
464

465
func getConfigPath() string {
12✔
466
        user, err := user.Current()
12✔
467
        if err != nil {
12✔
468
                log.Fatalf("%v", err)
×
469
        }
×
470

471
        return path.Join(user.HomeDir, ".pact", "pact-go.yml")
12✔
472
}
473

474
func (configuration) readConfig() pactConfig {
6✔
475
        pactConfigPath := getConfigPath()
6✔
476
        c := pactConfig{
6✔
477
                Libraries: map[string]packageMetadata{},
6✔
478
        }
6✔
479

6✔
480
        bytes, err := os.ReadFile(pactConfigPath)
6✔
481
        if err != nil {
6✔
482
                log.Println("[DEBUG] error reading file", pactConfigPath, "error: ", err)
×
483
                return c
×
484
        }
×
485

486
        err = yaml.Unmarshal(bytes, &c)
6✔
487
        if err != nil {
6✔
488
                log.Println("[DEBUG] error unmarshalling YAML", pactConfigPath, "error: ", err)
×
489
        }
×
490
        return c
6✔
491
}
492

493
func (configuration) writeConfig(c pactConfig) error {
6✔
494
        log.Println("[DEBUG] writing config", c)
6✔
495
        pactConfigPath := getConfigPath()
6✔
496

6✔
497
        err := os.MkdirAll(filepath.Dir(pactConfigPath), 0755)
6✔
498
        if err != nil {
6✔
499
                log.Println("[DEBUG] error creating pact config directory")
×
500
                return err
×
501
        }
×
502

503
        bytes, err := yaml.Marshal(c)
6✔
504
        if err != nil {
6✔
505
                log.Println("[DEBUG] error marshalling YAML", pactConfigPath, "error: ", err)
×
506
                return err
×
507
        }
×
508
        log.Println("[DEBUG] writing yaml config to file", string(bytes))
6✔
509

6✔
510
        return os.WriteFile(pactConfigPath, bytes, 0644)
6✔
511
}
512

513
type hasher interface {
514
        hash(src string) (string, error)
515
}
516

517
type defaultHasher struct{}
518

519
func (d *defaultHasher) hash(src string) (string, error) {
6✔
520
        log.Println("[DEBUG] obtaining hash for file", src)
6✔
521

6✔
522
        f, err := os.Open(src)
6✔
523
        if err != nil {
6✔
524
                return "", err
×
525
        }
×
526
        defer f.Close()
6✔
527

6✔
528
        h := md5.New()
6✔
529
        if _, err := io.Copy(h, f); err != nil {
6✔
530
                return "", err
×
531
        }
×
532

533
        return fmt.Sprintf("%x", h.Sum(nil)), nil
6✔
534
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc