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

pact-foundation / pact-go / 18960722150

31 Oct 2025 02:20AM UTC coverage: 30.702% (+2.4%) from 28.272%
18960722150

Pull #455

github

YOU54F
chore: make the linter happy
Pull Request #455: Feat/purego

386 of 514 new or added lines in 8 files covered. (75.1%)

3 existing lines in 2 files now uncovered.

2003 of 6524 relevant lines covered (30.7%)

16.44 hits per line

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

56.13
/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 {
12✔
29
        _, file, _, ok := runtime.Caller(0)
12✔
30
        if !ok {
12✔
31
                return ""
×
32
        }
×
33
        pactRoot := filepath.Dir(filepath.Dir(file))
12✔
34
        return filepath.Join(pactRoot, "internal", "native")
12✔
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) {
12✔
54
        i := &Installer{downloader: &defaultDownloader{}, fs: afero.NewOsFs(), hasher: &defaultHasher{}, config: &configuration{}}
12✔
55

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

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

12✔
69
        if !strings.Contains(runtime.GOARCH, "64") {
12✔
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 {
12✔
74
        case "amd64":
8✔
75
                i.arch = x86_64
8✔
76
        case "arm64":
4✔
77
                i.arch = aarch64
4✔
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
12✔
84
}
85

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

91
// SetConfigLibDir stores the libDir in the config
NEW
92
func (i *Installer) SetConfigLibDir(dir string) {
×
NEW
93
        // print debug log
×
NEW
94
        log.Printf("[DEBUG] setting config lib dir to %s", dir)
×
NEW
95
        c := i.config.readConfig()
×
NEW
96
        c.LibDir = dir
×
NEW
97
        if err := i.config.writeConfig(c); err != nil {
×
NEW
98
                log.Printf("[ERROR] failed to write config: %v", err)
×
NEW
99
        }
×
100
}
101

102
// GetConfigLibDir retrieves the libDir from config
NEW
103
func GetConfigLibDir() string {
×
NEW
104
        c := &configuration{}
×
NEW
105
        config := c.readConfig()
×
NEW
106
        return config.LibDir
×
NEW
107
}
×
108

109
// GetActualLibDir returns the actual library directory being used
NEW
110
func (i *Installer) GetActualLibDir() string {
×
NEW
111
        return i.getLibDir()
×
NEW
112
}
×
113

114
// Force installs over the top
115
func (i *Installer) Force(force bool) {
×
116
        i.force = force
×
117
}
×
118

119
// CheckInstallation checks installation of all of the required libraries
120
// and downloads if they aren't present
121
func (i *Installer) CheckInstallation() error {
36✔
122

36✔
123
        // Check if files exist
36✔
124
        // --> Check if existing installed files
36✔
125
        if !i.force {
72✔
126
                if err := i.CheckPackageInstall(); err == nil {
48✔
127
                        return nil
12✔
128
                }
12✔
129
        }
130

131
        // Download dependencies
132
        if err := i.downloadDependencies(); err != nil {
24✔
133
                return err
×
134
        }
×
135

136
        // Install dependencies
137
        if err := i.installDependencies(); err != nil {
28✔
138
                return err
4✔
139
        }
4✔
140

141
        // Double check files landed correctly (can't execute 'version' call here,
142
        // because of dependency on the native libs we're trying to download!)
143
        if err := i.CheckPackageInstall(); err != nil {
28✔
144
                return fmt.Errorf("unable to verify downloaded/installed dependencies: %s", err)
8✔
145
        }
8✔
146

147
        return nil
12✔
148
}
149

150
func (i *Installer) getLibDir() string {
140✔
151
        if i.libDir != "" {
140✔
152
                return i.libDir
×
153
        }
×
154

155
        env := os.Getenv(downloadEnvVar)
140✔
156
        if env != "" {
140✔
UNCOV
157
                return env
×
UNCOV
158
        }
×
159
        homeDir, err := os.UserHomeDir()
140✔
160
        if err != nil {
140✔
NEW
161
                log.Printf("[WARN] Could not determine home directory: %v", err)
×
NEW
162
                return ""
×
NEW
163
        }
×
164

165
        return filepath.Join(homeDir, ".pact", "pact-go", "libs")
140✔
166
}
167

168
// CheckPackageInstall discovers any existing packages, and checks installation of a given binary using semver-compatible checks
169
func (i *Installer) CheckPackageInstall() error {
56✔
170
        for pkg, info := range packages {
112✔
171

56✔
172
                dst, _ := i.getLibDstForPackage(pkg)
56✔
173

56✔
174
                if _, err := i.fs.Stat(dst); err != nil {
88✔
175
                        log.Println("[INFO] package", info.libName, "not found")
32✔
176
                        return err
32✔
177
                } else {
56✔
178
                        log.Println("[INFO] package", info.libName, "found")
24✔
179
                }
24✔
180

181
                lib, ok := i.config.readConfig().Libraries[pkg]
24✔
182
                if ok {
24✔
183
                        if err := checkVersion(info.libName, lib.Version, info.semverRange); err != nil {
×
184
                                return err
×
185
                        }
×
186
                        log.Println("[INFO] package", info.libName, "is correctly installed")
×
187
                } else {
24✔
188
                        log.Println("[INFO] no package metadata information was found, run `pact-go install -f` to correct")
24✔
189
                }
24✔
190

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

×
198
                        if ok {
×
199
                                log.Println("[INFO] checking version", lib.Version(), "for lib", info.libName, "within semver range", info.semverRange)
×
200
                                if err := checkVersion(info.libName, lib.Version(), info.semverRange); err != nil {
×
201
                                        return err
×
202
                                }
×
203
                        } else {
×
204
                                log.Println("[DEBUG] unable to determine current version of package", pkg, "in LibRegistry", LibRegistry)
×
205
                        }
×
206

207
                        // Correct the configuration to reduce drift
208
                        err := i.updateConfiguration(dst, pkg, info)
×
209
                        if err != nil {
×
210
                                return err
×
211
                        }
×
212
                }
213
        }
214

215
        return nil
24✔
216
}
217

218
// Download all dependencies, and update the pact-go configuration file
219
func (i *Installer) downloadDependencies() error {
36✔
220
        for pkg, pkgInfo := range packages {
72✔
221
                src, err := i.getDownloadURLForPackage(pkg)
36✔
222

36✔
223
                if err != nil {
36✔
224
                        return err
×
225
                }
×
226

227
                dst, err := i.getLibDstForPackage(pkg)
36✔
228

36✔
229
                if err != nil {
36✔
230
                        return err
×
231
                }
×
232

233
                err = i.downloader.download(src, dst)
36✔
234

36✔
235
                if err != nil {
36✔
236
                        return err
×
237
                }
×
238

239
                err = os.Chmod(dst, 0755)
36✔
240
                if err != nil {
44✔
241
                        log.Println("[WARN] unable to set permissions on file", dst, "due to error:", err)
8✔
242
                }
8✔
243

244
                err = i.updateConfiguration(dst, pkg, pkgInfo)
36✔
245

36✔
246
                if err != nil {
36✔
247
                        return err
×
248
                }
×
249
        }
250

251
        return nil
36✔
252
}
253

254
func (i *Installer) installDependencies() error {
24✔
255
        if i.os == macos {
48✔
256
                for pkg, info := range packages {
48✔
257
                        log.Println("[INFO] setting install_name on library", info.libName, "for macos")
24✔
258

24✔
259
                        dst, err := i.getLibDstForPackage(pkg)
24✔
260

24✔
261
                        if err != nil {
24✔
262
                                return err
×
263
                        }
×
264

265
                        err = setMacOSInstallName(dst)
24✔
266

24✔
267
                        if err != nil {
28✔
268
                                return err
4✔
269
                        }
4✔
270
                }
271
        }
272

273
        return nil
20✔
274
}
275

276
// returns src
277
func (i *Installer) getDownloadURLForPackage(pkg string) (string, error) {
96✔
278
        pkgInfo, ok := packages[pkg]
96✔
279
        if !ok {
96✔
280
                return "", fmt.Errorf("unable to find package details for package: %s", pkg)
×
281
        }
×
282

283
        if checkMusl() && i.os == linux {
96✔
284
                return fmt.Sprintf(downloadTemplate, pkg, pkgInfo.version, osToLibName[i.os], i.os, i.arch+"-musl", osToExtension[i.os]), nil
×
285
        } else {
96✔
286
                return fmt.Sprintf(downloadTemplate, pkg, pkgInfo.version, osToLibName[i.os], i.os, i.arch, osToExtension[i.os]), nil
96✔
287

96✔
288
        }
96✔
289

290
}
291

292
func (i *Installer) getLibDstForPackage(pkg string) (string, error) {
140✔
293
        _, ok := packages[pkg]
140✔
294
        if !ok {
140✔
295
                return "", fmt.Errorf("unable to find package details for package: %s", pkg)
×
296
        }
×
297

298
        return path.Join(i.getLibDir(), osToLibName[i.os]) + "." + osToExtension[i.os], nil
140✔
299
}
300

301
// Write the metadata to reduce drift
302
func (i *Installer) updateConfiguration(dst string, pkg string, info packageInfo) error {
36✔
303
        // Get hash of file
36✔
304
        fmt.Println(i.hasher)
36✔
305
        hash, err := i.hasher.hash(dst)
36✔
306
        if err != nil {
36✔
307
                return err
×
308
        }
×
309

310
        // Read metadata
311
        c := i.config.readConfig()
36✔
312

36✔
313
        // Update config
36✔
314
        c.Libraries[pkg] = packageMetadata{
36✔
315
                LibName: info.libName,
36✔
316
                Version: info.version,
36✔
317
                Hash:    hash,
36✔
318
        }
36✔
319

36✔
320
        // Write metadata
36✔
321
        return i.config.writeConfig(c)
36✔
322
}
323

324
var setMacOSInstallName = func(file string) error {
12✔
325
        cmd := exec.Command("install_name_tool", "-id", file, file)
12✔
326
        log.Println("[DEBUG] running command:", cmd)
12✔
327
        stdoutStderr, err := cmd.CombinedOutput()
12✔
328

12✔
329
        if err != nil {
16✔
330
                return fmt.Errorf("error setting install name on pact lib: %s", err)
4✔
331
        }
4✔
332

333
        log.Println("[DEBUG] output from command", stdoutStderr)
8✔
334

8✔
335
        return err
8✔
336
}
337

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

×
341
        v, err := goversion.NewVersion(version)
×
342
        if err != nil {
×
343
                return err
×
344
        }
×
345

346
        constraints, err := goversion.NewConstraint(versionRange)
×
347
        if err != nil {
×
348
                return err
×
349
        }
×
350

351
        if constraints.Check(v) {
×
352
                log.Println("[DEBUG]", v, "satisfies constraints", v, constraints)
×
353
                return nil
×
354
        }
×
355

356
        return fmt.Errorf("version %s of %s does not match constraint %s", version, lib, versionRange)
×
357
}
358

359
// checkMusl checks if the OS uses musl library instead of glibc
360
func checkMusl() bool {
120✔
361
        lddPath, err := exec.LookPath("ldd")
120✔
362
        if err != nil {
200✔
363
                return false
80✔
364
        }
80✔
365

366
        cmd := exec.Command(lddPath, "/bin/echo")
40✔
367
        out, err := cmd.CombinedOutput()
40✔
368

40✔
369
        if err != nil {
40✔
370
                return false
×
371
        }
×
372
        if strings.Contains(string(out), "musl") {
40✔
373
                return true
×
374
        }
×
375

376
        return false
40✔
377
}
378

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

382
var supportedOSes = map[string]string{
383
        "darwin": macos,
384
        windows:  windows,
385
        linux:    linux,
386
}
387

388
var osToExtension = map[string]string{
389
        windows: "dll",
390
        linux:   "so",
391
        macos:   "dylib",
392
}
393

394
var osToLibName = map[string]string{
395
        windows: "pact_ffi",
396
        linux:   "libpact_ffi",
397
        macos:   "libpact_ffi",
398
}
399

400
type packageInfo struct {
401
        libName     string
402
        version     string
403
        semverRange string
404
}
405

406
const (
407
        FFIPackage     = "libpact_ffi"
408
        downloadEnvVar = "PACT_GO_LIB_DOWNLOAD_PATH"
409
        linux          = "linux"
410
        windows        = "windows"
411
        macos          = "macos"
412
        x86_64         = "x86_64"
413
        aarch64        = "aarch64"
414
)
415

416
var packages = map[string]packageInfo{
417
        FFIPackage: {
418
                libName:     "libpact_ffi",
419
                version:     "0.4.28",
420
                semverRange: ">= 0.4.0, < 1.0.0",
421
        },
422
}
423

424
type Versioner interface {
425
        Version() string
426
}
427

428
var LibRegistry = map[string]Versioner{}
429

430
type downloader interface {
431
        download(src string, dst string) error
432
}
433

434
type defaultDownloader struct{}
435

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

×
439
        baseDir := path.Dir(dst)
×
440
        if err := os.MkdirAll(baseDir, 0755); err != nil {
×
441
                return fmt.Errorf("failed to create %s; %w", baseDir, err)
×
442
        }
×
443

444
        f, err := os.Create(dst)
×
445
        if err != nil {
×
446
                return fmt.Errorf("failed to create output file; %w", err)
×
447
        }
×
448
        defer f.Close()
×
449

×
450
        resp, err := http.Get(src)
×
451
        if err != nil {
×
452
                return fmt.Errorf("failed http call to %s; %w", src, err)
×
453
        }
×
454
        defer resp.Body.Close()
×
455

×
456
        archive, err := gzip.NewReader(resp.Body)
×
457
        if err != nil {
×
458
                return fmt.Errorf("failed to create new gzip reader; %w", err)
×
459
        }
×
460

461
        _, err = io.Copy(f, archive)
×
462
        if err != nil {
×
463
                return fmt.Errorf("failed to copy archive to file; %w", err)
×
464
        }
×
465

466
        return nil
×
467
}
468

469
type packageMetadata struct {
470
        LibName string
471
        Version string
472
        Hash    string
473
}
474

475
type pactConfig struct {
476
        Libraries map[string]packageMetadata
477
        LibDir    string `yaml:"libDir,omitempty"`
478
}
479

480
type configReader interface {
481
        readConfig() pactConfig
482
}
483
type configWriter interface {
484
        writeConfig(pactConfig) error
485
}
486

487
type configReadWriter interface {
488
        configReader
489
        configWriter
490
}
491

492
type configuration struct{}
493

494
func getConfigPath() string {
24✔
495
        user, err := user.Current()
24✔
496
        if err != nil {
24✔
497
                log.Fatalf("%v", err)
×
498
        }
×
499

500
        return path.Join(user.HomeDir, ".pact", "pact-go.yml")
24✔
501
}
502

503
func (configuration) readConfig() pactConfig {
12✔
504
        pactConfigPath := getConfigPath()
12✔
505
        c := pactConfig{
12✔
506
                Libraries: map[string]packageMetadata{},
12✔
507
        }
12✔
508

12✔
509
        bytes, err := os.ReadFile(pactConfigPath)
12✔
510
        if err != nil {
12✔
511
                log.Println("[DEBUG] error reading file", pactConfigPath, "error: ", err)
×
512
                return c
×
513
        }
×
514

515
        err = yaml.Unmarshal(bytes, &c)
12✔
516
        if err != nil {
12✔
517
                log.Println("[DEBUG] error unmarshalling YAML", pactConfigPath, "error: ", err)
×
518
        }
×
519
        return c
12✔
520
}
521

522
func (configuration) writeConfig(c pactConfig) error {
12✔
523
        log.Println("[DEBUG] writing config", c)
12✔
524
        pactConfigPath := getConfigPath()
12✔
525

12✔
526
        err := os.MkdirAll(filepath.Dir(pactConfigPath), 0755)
12✔
527
        if err != nil {
12✔
528
                log.Println("[DEBUG] error creating pact config directory")
×
529
                return err
×
530
        }
×
531

532
        bytes, err := yaml.Marshal(c)
12✔
533
        if err != nil {
12✔
534
                log.Println("[DEBUG] error marshalling YAML", pactConfigPath, "error: ", err)
×
535
                return err
×
536
        }
×
537
        log.Println("[DEBUG] writing yaml config to file", string(bytes))
12✔
538

12✔
539
        return os.WriteFile(pactConfigPath, bytes, 0644)
12✔
540
}
541

542
type hasher interface {
543
        hash(src string) (string, error)
544
}
545

546
type defaultHasher struct{}
547

548
func (d *defaultHasher) hash(src string) (string, error) {
12✔
549
        log.Println("[DEBUG] obtaining hash for file", src)
12✔
550

12✔
551
        f, err := os.Open(src)
12✔
552
        if err != nil {
12✔
553
                return "", err
×
554
        }
×
555
        defer f.Close()
12✔
556

12✔
557
        h := md5.New()
12✔
558
        if _, err := io.Copy(h, f); err != nil {
12✔
559
                return "", err
×
560
        }
×
561

562
        return fmt.Sprintf("%x", h.Sum(nil)), nil
12✔
563
}
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