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

JuliaLang / julia / #37606

31 Aug 2023 03:12AM UTC coverage: 86.167% (-0.03%) from 86.2%
#37606

push

local

web-flow
Refine effects based on optimizer-derived information (#50805)

The optimizer may be able to derive information that is not available to
inference. For example, it may SROA a mutable value to derive additional
constant information. Additionally, some effects, like :consistent are
path-dependent and should ideally be scanned once all optimizations are
done. Now, there is a bit of a complication that we have generally so
far taken the position that the optimizer may do non-IPO-safe
optimizations, although in practice we never actually implemented any.
This was a sensible choice, because we weren't really doing anything
with the post-optimized IR other than feeding it into codegen anyway.
However, with irinterp and this change, there's now two consumers of
IPO-safely optimized IR. I do still think we may at some point want to
run passes that allow IPO-unsafe optimizations, but we can always add
them at the end of the pipeline.

With these changes, the effect analysis is a lot more precise. For
example, we can now derive :consistent for these functions:
```
function f1(b)
    if Base.inferencebarrier(b)
        error()
    end
    return b
end

function f3(x)
    @fastmath sqrt(x)
    return x
end
```
and we can derive `:nothrow` for this function:
```
function f2()
    if Ref(false)[]
        error()
    end
    return true
end
```

414 of 414 new or added lines in 13 files covered. (100.0%)

73383 of 85164 relevant lines covered (86.17%)

12690106.67 hits per line

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

79.19
/base/binaryplatforms.jl
1
# This file is a part of Julia. License is MIT: https://julialang.org/license
2

3
module BinaryPlatforms
4

5
export AbstractPlatform, Platform, HostPlatform, platform_dlext, tags, arch, os,
6
       os_version, libc, libgfortran_version, libstdcxx_version,
7
       cxxstring_abi, parse_dl_name_version, detect_libgfortran_version,
8
       detect_libstdcxx_version, detect_cxxstring_abi, call_abi, wordsize, triplet,
9
       select_platform, platforms_match, platform_name
10
import .Libc.Libdl
11

12
### Submodule with information about CPU features
13
include("cpuid.jl")
14
using .CPUID
15

16
# This exists to ease compatibility with old-style Platform objects
17
abstract type AbstractPlatform; end
18

19
"""
20
    Platform
21

22
A `Platform` represents all relevant pieces of information that a julia process may need
23
to know about its execution environment, such as the processor architecture, operating
24
system, libc implementation, etc...  It is, at its heart, a key-value mapping of tags
25
(such as `arch`, `os`, `libc`, etc...) to values (such as `"arch" => "x86_64"`, or
26
`"os" => "windows"`, etc...).  `Platform` objects are extensible in that the tag mapping
27
is open for users to add their own mappings to, as long as the mappings do not conflict
28
with the set of reserved tags: `arch`, `os`, `os_version`, `libc`, `call_abi`,
29
`libgfortran_version`, `libstdcxx_version`, `cxxstring_abi` and `julia_version`.
30

31
Valid tags and values are composed of alphanumeric and period characters.  All tags and
32
values will be lowercased when stored to reduce variation.
33

34
Example:
35

36
    Platform("x86_64", "windows"; cuda = "10.1")
37
"""
38
struct Platform <: AbstractPlatform
39
    tags::Dict{String,String}
40
    # The "compare strategy" allows selective overriding on how a tag is compared
41
    compare_strategies::Dict{String,Function}
42

43
    # Passing `tags` as a `Dict` avoids the need to infer different NamedTuple specializations
44
    function Platform(arch::String, os::String, _tags::Dict{String};
3,202✔
45
                      validate_strict::Bool = false,
46
                      compare_strategies::Dict{String,<:Function} = Dict{String,Function}())
47
        # A wee bit of normalization
48
        os = lowercase(os)
1,601✔
49
        arch = CPUID.normalize_arch(arch)
1,601✔
50

51
        tags = Dict{String,String}(
1,601✔
52
            "arch" => arch,
53
            "os" => os,
54
        )
55
        for (tag, value) in _tags
3,033✔
56
            value = value::Union{String,VersionNumber,Nothing}
3,040✔
57
            tag = lowercase(tag)
4,600✔
58
            if tag ∈ ("arch", "os")
13,795✔
59
                throw(ArgumentError("Cannot double-pass key $(tag)"))
3✔
60
            end
61

62
            # Drop `nothing` values; this means feature is not present or use default value.
63
            if value === nothing
3,037✔
64
                continue
978✔
65
            end
66

67
            # Normalize things that are known to be version numbers so that comparisons are easy.
68
            # Note that in our effort to be extremely compatible, we actually allow something that
69
            # doesn't parse nicely into a VersionNumber to persist, but if `validate_strict` is
70
            # set to `true`, it will cause an error later on.
71
            if tag ∈ ("libgfortran_version", "libstdcxx_version", "os_version")
10,303✔
72
                if isa(value, VersionNumber)
995✔
73
                    value = string(value)
983✔
74
                elseif isa(value, String)
×
75
                    v = tryparse(VersionNumber, value)
675✔
76
                    if isa(v, VersionNumber)
675✔
77
                        value = string(v)
673✔
78
                    end
79
                end
80
            end
81

82
            # Use `add_tag!()` to add the tag to our collection of tags
83
            add_tag!(tags, tag, string(value)::String)
3,619✔
84
        end
7,765✔
85

86
        # Auto-map call_abi and libc where necessary:
87
        if os == "linux" && !haskey(tags, "libc")
1,598✔
88
            # Default to `glibc` on Linux
89
            tags["libc"] = "glibc"
173✔
90
        end
91
        if os == "linux" && arch ∈ ("armv7l", "armv6l") && "call_abi" ∉ keys(tags)
3,306✔
92
            # default `call_abi` to `eabihf` on 32-bit ARM
93
            tags["call_abi"] = "eabihf"
104✔
94
        end
95

96
        # If the user is asking for strict validation, do so.
97
        if validate_strict
1,598✔
98
            validate_tags(tags)
264✔
99
        end
100

101
        # By default, we compare julia_version only against major and minor versions:
102
        if haskey(tags, "julia_version") && !haskey(compare_strategies, "julia_version")
1,577✔
103
            compare_strategies["julia_version"] = (a::String, b::String, a_comparator, b_comparator) -> begin
84✔
104
                a = VersionNumber(a)
2✔
105
                b = VersionNumber(b)
2✔
106
                return a.major == b.major && a.minor == b.minor
2✔
107
            end
108
        end
109

110
        return new(tags, compare_strategies)
1,577✔
111
    end
112
end
113

114
# Keyword interface (to avoid inference of specialized NamedTuple methods, use the Dict interface for `tags`)
115
function Platform(arch::String, os::String;
1,458✔
116
                  validate_strict::Bool = false,
117
                  compare_strategies::Dict{String,<:Function} = Dict{String,Function}(),
118
                  kwargs...)
119
    tags = Dict{String,Any}(String(tag)::String=>tagvalue(value) for (tag, value) in kwargs)
729✔
120
    return Platform(arch, os, tags; validate_strict, compare_strategies)
729✔
121
end
122

123
tagvalue(v::Union{String,VersionNumber,Nothing}) = v
1,955✔
124
tagvalue(v::Symbol) = String(v)
19✔
125
tagvalue(v::AbstractString) = convert(String, v)::String
×
126

127
# Simple tag insertion that performs a little bit of validation
128
function add_tag!(tags::Dict{String,String}, tag::String, value::String)
3,622✔
129
    # I know we said only alphanumeric and dots, but let's be generous so that we can expand
130
    # our support in the future while remaining as backwards-compatible as possible.  The
131
    # only characters that are absolutely disallowed right now are `-`, `+`, ` ` and things
132
    # that are illegal in filenames:
133
    nonos = raw"""+- /<>:"'\|?*"""
×
134
    if any(occursin(nono, tag) for nono in nonos)
3,622✔
135
        throw(ArgumentError("Invalid character in tag name \"$(tag)\"!"))
×
136
    end
137

138
    # Normalize and reject nonos
139
    value = lowercase(value)
3,622✔
140
    if any(occursin(nono, value) for nono in nonos)
3,622✔
141
        throw(ArgumentError("Invalid character in tag value \"$(value)\"!"))
1✔
142
    end
143
    tags[tag] = value
3,621✔
144
    return value
3,621✔
145
end
146

147
# Other `Platform` types can override this (I'm looking at you, `AnyPlatform`)
148
tags(p::Platform) = p.tags
6,112✔
149

150
# Make it act more like a dict
151
Base.getindex(p::AbstractPlatform, k::String) = getindex(tags(p), k)
3✔
152
Base.haskey(p::AbstractPlatform, k::String) = haskey(tags(p), k)
158✔
153
function Base.setindex!(p::AbstractPlatform, v::String, k::String)
3✔
154
    add_tag!(tags(p), k, v)
3✔
155
    return p
2✔
156
end
157

158
# Hash definition to ensure that it's stable
159
function Base.hash(p::Platform, h::UInt)
×
160
    h += 0x506c6174666f726d % UInt
×
161
    h = hash(p.tags, h)
482✔
162
    h = hash(p.compare_strategies, h)
482✔
163
    return h
×
164
end
165

166
# Simple equality definition; for compatibility testing, use `platforms_match()`
167
function Base.:(==)(a::Platform, b::Platform)
500✔
168
    return a.tags == b.tags && a.compare_strategies == b.compare_strategies
500✔
169
end
170

171

172
# Allow us to easily serialize Platform objects
173
function Base.repr(p::Platform; context=nothing)
2✔
174
    str = string(
1✔
175
        "Platform(",
176
        repr(arch(p)),
177
        ", ",
178
        repr(os(p)),
179
        "; ",
180
        join(("$(k) = $(repr(v))" for (k, v) in tags(p) if k ∉ ("arch", "os")), ", "),
181
        ")",
182
    )
183
end
184

185
# Make showing the platform a bit more palatable
186
function Base.show(io::IO, p::Platform)
×
187
    str = string(platform_name(p), " ", arch(p))
×
188
    # Add on all the other tags not covered by os/arch:
189
    other_tags = sort!(filter!(kv -> kv[1] ∉ ("os", "arch"), collect(tags(p))))
×
190
    if !isempty(other_tags)
×
191
        str = string(str, " {", join([string(k, "=", v) for (k, v) in other_tags], ", "), "}")
×
192
    end
193
    print(io, str)
×
194
end
195

196
function validate_tags(tags::Dict)
264✔
197
    throw_invalid_key(k) = throw(ArgumentError("Key \"$(k)\" cannot have value \"$(tags[k])\""))
267✔
198
    # Validate `arch`
199
    if tags["arch"] ∉ ("x86_64", "i686", "armv7l", "armv6l", "aarch64", "powerpc64le")
603✔
200
        throw_invalid_key("arch")
1✔
201
    end
202
    # Validate `os`
203
    if tags["os"] ∉ ("linux", "macos", "freebsd", "windows")
407✔
204
        throw_invalid_key("os")
1✔
205
    end
206
    # Validate `os`/`arch` combination
207
    throw_os_mismatch() = throw(ArgumentError("Invalid os/arch combination: $(tags["os"])/$(tags["arch"])"))
264✔
208
    if tags["os"] == "windows" && tags["arch"] ∉ ("x86_64", "i686", "armv7l", "aarch64")
276✔
209
        throw_os_mismatch()
1✔
210
    end
211
    if tags["os"] == "macos" && tags["arch"] ∉ ("x86_64", "aarch64")
270✔
212
        throw_os_mismatch()
1✔
213
    end
214

215
    # Validate `os`/`libc` combination
216
    throw_libc_mismatch() = throw(ArgumentError("Invalid os/libc combination: $(tags["os"])/$(tags["libc"])"))
263✔
217
    if tags["os"] == "linux"
260✔
218
        # Linux always has a `libc` entry
219
        if tags["libc"] ∉ ("glibc", "musl")
197✔
220
            throw_libc_mismatch()
1✔
221
        end
222
    else
223
        # Nothing else is allowed to have a `libc` entry
224
        if haskey(tags, "libc")
75✔
225
            throw_libc_mismatch()
2✔
226
        end
227
    end
228

229
    # Validate `os`/`arch`/`call_abi` combination
230
    throw_call_abi_mismatch() = throw(ArgumentError("Invalid os/arch/call_abi combination: $(tags["os"])/$(tags["arch"])/$(tags["call_abi"])"))
268✔
231
    if tags["os"] == "linux" && tags["arch"] ∈ ("armv7l", "armv6l")
557✔
232
        # If an ARM linux has does not have `call_abi` set to something valid, be sad.
233
        if !haskey(tags, "call_abi") || tags["call_abi"] ∉ ("eabihf", "eabi")
97✔
234
            throw_call_abi_mismatch()
43✔
235
        end
236
    else
237
        # Nothing else should have a `call_abi`.
238
        if haskey(tags, "call_abi")
214✔
239
            throw_call_abi_mismatch()
8✔
240
        end
241
    end
242

243
    # Validate `libgfortran_version` is a parsable `VersionNumber`
244
    throw_version_number(k) = throw(ArgumentError("\"$(k)\" cannot have value \"$(tags[k])\", must be a valid VersionNumber"))
248✔
245
    if "libgfortran_version" in keys(tags) && tryparse(VersionNumber, tags["libgfortran_version"]) === nothing
246✔
246
        throw_version_number("libgfortran_version")
1✔
247
    end
248

249
    # Validate `cxxstring_abi` is one of the two valid options:
250
    if "cxxstring_abi" in keys(tags) && tags["cxxstring_abi"] ∉ ("cxx03", "cxx11")
294✔
251
        throw_invalid_key("cxxstring_abi")
1✔
252
    end
253

254
    # Validate `libstdcxx_version` is a parsable `VersionNumber`
255
    if "libstdcxx_version" in keys(tags) && tryparse(VersionNumber, tags["libstdcxx_version"]) === nothing
244✔
256
        throw_version_number("libstdcxx_version")
1✔
257
    end
258
end
259

260
function set_compare_strategy!(p::Platform, key::String, f::Function)
1✔
261
    if !haskey(p.tags, key)
81✔
262
        throw(ArgumentError("Cannot set comparison strategy for nonexistent tag $(key)!"))
×
263
    end
264
    p.compare_strategies[key] = f
81✔
265
end
266

267
function get_compare_strategy(p::Platform, key::String, default = compare_default)
×
268
    if !haskey(p.tags, key)
3,826✔
269
        throw(ArgumentError("Cannot get comparison strategy for nonexistent tag $(key)!"))
×
270
    end
271
    return get(p.compare_strategies, key, default)
1,928✔
272
end
273
get_compare_strategy(p::AbstractPlatform, key::String, default = compare_default) = default
×
274

275

276

277
"""
278
    compare_default(a::String, b::String, a_requested::Bool, b_requested::Bool)
279

280
Default comparison strategy that falls back to `a == b`.  This only ever happens if both
281
`a` and `b` request this strategy, as any other strategy is preferable to this one.
282
"""
283
function compare_default(a::String, b::String, a_requested::Bool, b_requested::Bool)
926✔
284
    return a == b
926✔
285
end
286

287
"""
288
    compare_version_cap(a::String, b::String, a_comparator, b_comparator)
289

290
Example comparison strategy for `set_comparison_strategy!()` that implements a version
291
cap for host platforms that support _up to_ a particular version number.  As an example,
292
if an artifact is built for macOS 10.9, it can run on macOS 10.11, however if it were
293
built for macOS 10.12, it could not.  Therefore, the host platform of macOS 10.11 has a
294
version cap at `v"10.11"`.
295

296
Note that because both hosts and artifacts are represented with `Platform` objects it
297
is possible to call `platforms_match()` with two artifacts, a host and an artifact, an
298
artifact and a host, and even two hosts.  We attempt to do something intelligent for all
299
cases, but in the case of comparing version caps between two hosts, we return `true` only
300
if the two host platforms are in fact identical.
301
"""
302
function compare_version_cap(a::String, b::String, a_requested::Bool, b_requested::Bool)
10✔
303
    a = VersionNumber(a)
10✔
304
    b = VersionNumber(b)
10✔
305

306
    # If both b and a requested, then we fall back to equality:
307
    if a_requested && b_requested
10✔
308
        return a == b
×
309
    end
310

311
    # Otherwise, do the comparison between the the single version cap and the single version:
312
    if a_requested
10✔
313
        return b <= a
10✔
314
    else
315
        return a <= b
×
316
    end
317
end
318

319

320

321
"""
322
    HostPlatform(p::AbstractPlatform)
323

324
Convert a `Platform` to act like a "host"; e.g. if it has a version-bound tag such as
325
`"libstdcxx_version" => "3.4.26"`, it will treat that value as an upper bound, rather
326
than a characteristic.  `Platform` objects that define artifacts generally denote the
327
SDK or version that the artifact was built with, but for platforms, these versions are
328
generally the maximal version the platform can support.  The way this transformation
329
is implemented is to change the appropriate comparison strategies to treat these pieces
330
of data as bounds rather than points in any comparison.
331
"""
332
function HostPlatform(p::AbstractPlatform)
79✔
333
    if haskey(p, "os_version")
79✔
334
        set_compare_strategy!(p, "os_version", compare_version_cap)
1✔
335
    end
336
    if haskey(p, "libstdcxx_version")
79✔
337
        set_compare_strategy!(p, "libstdcxx_version", compare_version_cap)
79✔
338
    end
339
    return p
79✔
340
end
341

342
"""
343
    arch(p::AbstractPlatform)
344

345
Get the architecture for the given `Platform` object as a `String`.
346

347
# Examples
348
```jldoctest
349
julia> arch(Platform("aarch64", "Linux"))
350
"aarch64"
351

352
julia> arch(Platform("amd64", "freebsd"))
353
"x86_64"
354
```
355
"""
356
arch(p::AbstractPlatform) = get(tags(p), "arch", nothing)
244✔
357

358
"""
359
    os(p::AbstractPlatform)
360

361
Get the operating system for the given `Platform` object as a `String`.
362

363
# Examples
364
```jldoctest
365
julia> os(Platform("armv7l", "Linux"))
366
"linux"
367

368
julia> os(Platform("aarch64", "macos"))
369
"macos"
370
```
371
"""
372
os(p::AbstractPlatform) = get(tags(p), "os", nothing)
280✔
373

374
# As a special helper, it's sometimes useful to know the current OS at compile-time
375
function os()
×
376
    if Sys.iswindows()
×
377
        return "windows"
×
378
    elseif Sys.isapple()
×
379
        return "macos"
×
380
    elseif Sys.isbsd()
×
381
        return "freebsd"
×
382
    else
383
        return "linux"
×
384
    end
385
end
386

387
"""
388
    libc(p::AbstractPlatform)
389

390
Get the libc for the given `Platform` object as a `String`.  Returns `nothing` on
391
platforms with no explicit `libc` choices (which is most platforms).
392

393
# Examples
394
```jldoctest
395
julia> libc(Platform("armv7l", "Linux"))
396
"glibc"
397

398
julia> libc(Platform("aarch64", "linux"; libc="musl"))
399
"musl"
400

401
julia> libc(Platform("i686", "Windows"))
402
```
403
"""
404
libc(p::AbstractPlatform) = get(tags(p), "libc", nothing)
195✔
405

406
"""
407
    call_abi(p::AbstractPlatform)
408

409
Get the call ABI for the given `Platform` object as a `String`.  Returns `nothing` on
410
platforms with no explicit `call_abi` choices (which is most platforms).
411

412
# Examples
413
```jldoctest
414
julia> call_abi(Platform("armv7l", "Linux"))
415
"eabihf"
416

417
julia> call_abi(Platform("x86_64", "macos"))
418
```
419
"""
420
call_abi(p::AbstractPlatform) = get(tags(p), "call_abi", nothing)
119✔
421

422
const platform_names = Dict(
423
    "linux" => "Linux",
424
    "macos" => "macOS",
425
    "windows" => "Windows",
426
    "freebsd" => "FreeBSD",
427
    nothing => "Unknown",
428
)
429

430
"""
431
    platform_name(p::AbstractPlatform)
432

433
Get the "platform name" of the given platform, returning e.g. "Linux" or "Windows".
434
"""
435
function platform_name(p::AbstractPlatform)
4✔
436
    return platform_names[os(p)]
4✔
437
end
438

439
function VNorNothing(d::Dict, key)
×
440
    v = get(d, key, nothing)
481✔
441
    if v === nothing
296✔
442
        return nothing
111✔
443
    end
444
    return VersionNumber(v)::VersionNumber
185✔
445
end
446

447
"""
448
    libgfortran_version(p::AbstractPlatform)
449

450
Get the libgfortran version dictated by this `Platform` object as a `VersionNumber`,
451
or `nothing` if no compatibility bound is imposed.
452
"""
453
libgfortran_version(p::AbstractPlatform) = VNorNothing(tags(p), "libgfortran_version")
250✔
454

455
"""
456
    libstdcxx_version(p::AbstractPlatform)
457

458
Get the libstdc++ version dictated by this `Platform` object, or `nothing` if no
459
compatibility bound is imposed.
460
"""
461
libstdcxx_version(p::AbstractPlatform) = VNorNothing(tags(p), "libstdcxx_version")
226✔
462

463
"""
464
    cxxstring_abi(p::AbstractPlatform)
465

466
Get the c++ string ABI dictated by this `Platform` object, or `nothing` if no ABI is imposed.
467
"""
468
cxxstring_abi(p::AbstractPlatform) = get(tags(p), "cxxstring_abi", nothing)
229✔
469

470
"""
471
    os_version(p::AbstractPlatform)
472

473
Get the OS version dictated by this `Platform` object, or `nothing` if no OS version is
474
imposed/no data is available.  This is most commonly used by MacOS and FreeBSD objects
475
where we have high platform SDK fragmentation, and features are available only on certain
476
platform versions.
477
"""
478
os_version(p::AbstractPlatform) = VNorNothing(tags(p), "os_version")
5✔
479

480
"""
481
    wordsize(p::AbstractPlatform)
482

483
Get the word size for the given `Platform` object.
484

485
# Examples
486
```jldoctest
487
julia> wordsize(Platform("armv7l", "linux"))
488
32
489

490
julia> wordsize(Platform("x86_64", "macos"))
491
64
492
```
493
"""
494
wordsize(p::AbstractPlatform) = (arch(p) ∈ ("i686", "armv6l", "armv7l")) ? 32 : 64
10✔
495

496
"""
497
    triplet(p::AbstractPlatform)
498

499
Get the target triplet for the given `Platform` object as a `String`.
500

501
# Examples
502
```jldoctest
503
julia> triplet(Platform("x86_64", "MacOS"))
504
"x86_64-apple-darwin"
505

506
julia> triplet(Platform("i686", "Windows"))
507
"i686-w64-mingw32"
508

509
julia> triplet(Platform("armv7l", "Linux"; libgfortran_version="3"))
510
"armv7l-linux-gnueabihf-libgfortran3"
511
```
512
"""
513
function triplet(p::AbstractPlatform)
100✔
514
    str = string(
105✔
515
        arch(p)::Union{Symbol,String},
516
        os_str(p),
517
        libc_str(p),
518
        call_abi_str(p),
519
    )
520

521
    # Tack on optional compiler ABI flags
522
    if libgfortran_version(p) !== nothing
150✔
523
        str = string(str, "-libgfortran", libgfortran_version(p).major)
100✔
524
    end
525
    if cxxstring_abi(p) !== nothing
143✔
526
        str = string(str, "-", cxxstring_abi(p))
86✔
527
    end
528
    if libstdcxx_version(p) !== nothing
142✔
529
        str = string(str, "-libstdcxx", libstdcxx_version(p).patch)
84✔
530
    end
531

532
    # Tack on all extra tags
533
    for (tag, val) in tags(p)
200✔
534
        if tag ∈ ("os", "arch", "libc", "call_abi", "libgfortran_version", "libstdcxx_version", "cxxstring_abi", "os_version")
1,641✔
535
            continue
436✔
536
        end
537
        str = string(str, "-", tag, "+", val)
25✔
538
    end
822✔
539
    return str
100✔
540
end
541

542
function os_str(p::AbstractPlatform)
100✔
543
    if os(p) == "linux"
200✔
544
        return "-linux"
95✔
545
    elseif os(p) == "macos"
10✔
546
        osvn = os_version(p)
3✔
547
        if osvn !== nothing
2✔
548
            return "-apple-darwin$(osvn.major)"
1✔
549
        else
550
            return "-apple-darwin"
1✔
551
        end
552
    elseif os(p) == "windows"
6✔
553
        return "-w64-mingw32"
1✔
554
    elseif os(p) == "freebsd"
4✔
555
        osvn = os_version(p)
2✔
556
        if osvn !== nothing
2✔
557
            return "-unknown-freebsd$(osvn.major).$(osvn.minor)"
×
558
        else
559
            return "-unknown-freebsd"
2✔
560
        end
561
    else
562
        return "-unknown"
×
563
    end
564
end
565

566
# Helper functions for Linux and FreeBSD libc/abi mishmashes
567
function libc_str(p::AbstractPlatform)
×
568
    lc = libc(p)
195✔
569
    if lc === nothing
100✔
570
        return ""
5✔
571
    elseif lc === "glibc"
95✔
572
        return "-gnu"
91✔
573
    else
574
        return string("-", lc)
4✔
575
    end
576
end
577
function call_abi_str(p::AbstractPlatform)
×
578
    cabi = call_abi(p)
105✔
579
    cabi === nothing ? "" : string(cabi::Union{Symbol,String})
105✔
580
end
581

582
Sys.isapple(p::AbstractPlatform) = os(p) == "macos"
2✔
583
Sys.islinux(p::AbstractPlatform) = os(p) == "linux"
2✔
584
Sys.iswindows(p::AbstractPlatform) = os(p) == "windows"
2✔
585
Sys.isfreebsd(p::AbstractPlatform) = os(p) == "freebsd"
×
586
Sys.isbsd(p::AbstractPlatform) = os(p) ∈ ("freebsd", "macos")
3✔
587
Sys.isunix(p::AbstractPlatform) = Sys.isbsd(p) || Sys.islinux(p)
×
588

589
const arch_mapping = Dict(
590
    "x86_64" => "(x86_|amd)64",
591
    "i686" => "i\\d86",
592
    "aarch64" => "(aarch64|arm64)",
593
    "armv7l" => "arm(v7l)?", # if we just see `arm-linux-gnueabihf`, we assume it's `armv7l`
594
    "armv6l" => "armv6l",
595
    "powerpc64le" => "p(ower)?pc64le",
596
)
597
# Keep this in sync with `CPUID.ISAs_by_family`
598
# These are the CPUID side of the microarchitectures targeted by GCC flags in BinaryBuilder.jl
599
const arch_march_isa_mapping = let
600
    function get_set(arch, name)
×
601
        all = CPUID.ISAs_by_family[arch]
×
602
        return all[findfirst(x -> x.first == name, all)].second
×
603
    end
604
    Dict(
605
        "i686" => [
606
            "pentium4" => get_set("i686", "pentium4"),
607
            "prescott" => get_set("i686", "prescott"),
608
        ],
609
        "x86_64" => [
610
            "x86_64" => get_set("x86_64", "x86_64"),
611
            "avx" => get_set("x86_64", "sandybridge"),
612
            "avx2" => get_set("x86_64", "haswell"),
613
            "avx512" => get_set("x86_64", "skylake_avx512"),
614
        ],
615
        "armv6l" => [
616
            "arm1176jzfs" => get_set("armv6l", "arm1176jzfs"),
617
        ],
618
        "armv7l" => [
619
            "armv7l" => get_set("armv7l", "armv7l"),
620
            "neonvfpv4" => get_set("armv7l", "armv7l+neon+vfpv4"),
621
        ],
622
        "aarch64" => [
623
            "armv8_0" => get_set("aarch64", "armv8.0-a"),
624
            "armv8_1" => get_set("aarch64", "armv8.1-a"),
625
            "armv8_2_crypto" => get_set("aarch64", "armv8.2-a+crypto"),
626
            "a64fx" => get_set("aarch64", "a64fx"),
627
            "apple_m1" => get_set("aarch64", "apple_m1"),
628
        ],
629
        "powerpc64le" => [
630
            "power8" => get_set("powerpc64le", "power8"),
631
        ]
632
    )
633
end
634
const os_mapping = Dict(
635
    "macos" => "-apple-darwin[\\d\\.]*",
636
    "freebsd" => "-(.*-)?freebsd[\\d\\.]*",
637
    "windows" => "-w64-mingw32",
638
    "linux" => "-(.*-)?linux",
639
)
640
const libc_mapping = Dict(
641
    "libc_nothing" => "",
642
    "glibc" => "-gnu",
643
    "musl" => "-musl",
644
)
645
const call_abi_mapping = Dict(
646
    "call_abi_nothing" => "",
647
    "eabihf" => "eabihf",
648
    "eabi" => "eabi",
649
)
650
const libgfortran_version_mapping = Dict(
651
    "libgfortran_nothing" => "",
652
    "libgfortran3" => "(-libgfortran3)|(-gcc4)", # support old-style `gccX` versioning
653
    "libgfortran4" => "(-libgfortran4)|(-gcc7)",
654
    "libgfortran5" => "(-libgfortran5)|(-gcc8)",
655
)
656
const cxxstring_abi_mapping = Dict(
657
    "cxxstring_nothing" => "",
658
    "cxx03" => "-cxx03",
659
    "cxx11" => "-cxx11",
660
)
661
const libstdcxx_version_mapping = Dict{String,String}(
662
    "libstdcxx_nothing" => "",
663
    "libstdcxx" => "-libstdcxx\\d+",
664
)
665

666
"""
667
    parse(::Type{Platform}, triplet::AbstractString)
668

669
Parses a string platform triplet back into a `Platform` object.
670
"""
671
function Base.parse(::Type{Platform}, triplet::String; validate_strict::Bool = false)
334✔
672
    # Helper function to collapse dictionary of mappings down into a regex of
673
    # named capture groups joined by "|" operators
674
    c(mapping) = string("(",join(["(?<$k>$v)" for (k, v) in mapping], "|"), ")")
1,169✔
675

676
    # We're going to build a mondo regex here to parse everything:
677
    triplet_regex = Regex(string(
167✔
678
        "^",
679
        # First, the core triplet; arch/os/libc/call_abi
680
        c(arch_mapping),
681
        c(os_mapping),
682
        c(libc_mapping),
683
        c(call_abi_mapping),
684
        # Next, optional things, like libgfortran/libstdcxx/cxxstring abi
685
        c(libgfortran_version_mapping),
686
        c(cxxstring_abi_mapping),
687
        c(libstdcxx_version_mapping),
688
        # Finally, the catch-all for extended tags
689
        "(?<tags>(?:-[^-]+\\+[^-]+)*)?",
690
        "\$",
691
    ))
692

693
    m = match(triplet_regex, triplet)
167✔
694
    if m !== nothing
167✔
695
        # Helper function to find the single named field within the giant regex
696
        # that is not `nothing` for each mapping we give it.
697
        get_field(m, mapping) = begin
1,312✔
698
            for k in keys(mapping)
2,296✔
699
                if m[k] !== nothing
2,471✔
700
                    # Convert our sentinel `nothing` values to actual `nothing`
701
                    if endswith(k, "_nothing")
1,148✔
702
                        return nothing
366✔
703
                    end
704
                    # Convert libgfortran/libstdcxx version numbers
705
                    if startswith(k, "libgfortran")
883✔
706
                        return VersionNumber(parse(Int,k[12:end]))
99✔
707
                    elseif startswith(k, "libstdcxx")
782✔
708
                        return VersionNumber(3, 4, parse(Int,m[k][11:end]))
97✔
709
                    else
710
                        return k
586✔
711
                    end
712
                end
713
            end
1,323✔
714
        end
715

716
        # Extract the information we're interested in:
717
        tags = Dict{String,Any}()
164✔
718
        arch = get_field(m, arch_mapping)
164✔
719
        os = get_field(m, os_mapping)
164✔
720
        tags["libc"] = get_field(m, libc_mapping)
317✔
721
        tags["call_abi"] = get_field(m, call_abi_mapping)
170✔
722
        tags["libgfortran_version"] = get_field(m, libgfortran_version_mapping)
263✔
723
        tags["libstdcxx_version"] = get_field(m, libstdcxx_version_mapping)
261✔
724
        tags["cxxstring_abi"] = get_field(m, cxxstring_abi_mapping)
263✔
725
        function split_tags(tagstr)
164✔
726
            tag_fields = split(tagstr, "-"; keepempty=false)
164✔
727
            if isempty(tag_fields)
164✔
728
                return Pair{String,String}[]
83✔
729
            end
730
            return map(v -> String(v[1]) => String(v[2]), split.(tag_fields, "+"))
163✔
731
        end
732
        merge!(tags, Dict(split_tags(m["tags"])))
328✔
733

734
        # Special parsing of os version number, if any exists
735
        function extract_os_version(os_name, pattern)
173✔
736
            m_osvn = match(pattern, m[os_name])
9✔
737
            if m_osvn !== nothing
9✔
738
                return VersionNumber(m_osvn.captures[1])
8✔
739
            end
740
            return nothing
1✔
741
        end
742
        os_version = nothing
×
743
        if os == "macos"
328✔
744
            os_version = extract_os_version("macos", r".*darwin([\d\.]+)"sa)
5✔
745
        end
746
        if os == "freebsd"
328✔
747
            os_version = extract_os_version("freebsd", r".*freebsd([\d.]+)"sa)
4✔
748
        end
749
        tags["os_version"] = os_version
172✔
750

751
        return Platform(arch, os, tags; validate_strict)
164✔
752
    end
753
    throw(ArgumentError("Platform `$(triplet)` is not an officially supported platform"))
3✔
754
end
755
Base.parse(::Type{Platform}, triplet::AbstractString; kwargs...) =
×
756
    parse(Platform, convert(String, triplet)::String; kwargs...)
757

758
function Base.tryparse(::Type{Platform}, triplet::AbstractString)
×
759
    try
×
760
        parse(Platform, triplet)
×
761
    catch e
762
        if isa(e, InterruptException)
×
763
            rethrow(e)
×
764
        end
765
        return nothing
×
766
    end
767
end
768

769
"""
770
    platform_dlext(p::AbstractPlatform = HostPlatform())
771

772
Return the dynamic library extension for the given platform, defaulting to the
773
currently running platform.  E.g. returns "so" for a Linux-based platform,
774
"dll" for a Windows-based platform, etc...
775
"""
776
function platform_dlext(p::AbstractPlatform = HostPlatform())
7✔
777
    if os(p) == "windows"
13✔
778
        return "dll"
1✔
779
    elseif os(p) == "macos"
10✔
780
        return "dylib"
1✔
781
    else
782
        return "so"
4✔
783
    end
784
end
785

786
"""
787
    parse_dl_name_version(path::String, platform::AbstractPlatform)
788

789
Given a path to a dynamic library, parse out what information we can
790
from the filename.  E.g. given something like "lib/libfoo.so.3.2",
791
this function returns `"libfoo", v"3.2"`.  If the path name is not a
792
valid dynamic library, this method throws an error.  If no soversion
793
can be extracted from the filename, as in "libbar.so" this method
794
returns `"libbar", nothing`.
795
"""
796
function parse_dl_name_version(path::String, os::String)
27✔
797
    # Use an extraction regex that matches the given OS
798
    local dlregex
×
799
    if os == "windows"
27✔
800
        # On Windows, libraries look like `libnettle-6.dll`
801
        dlregex = r"^(.*?)(?:-((?:[\.\d]+)*))?\.dll$"sa
6✔
802
    elseif os == "macos"
21✔
803
        # On OSX, libraries look like `libnettle.6.3.dylib`
804
        dlregex = r"^(.*?)((?:\.[\d]+)*)\.dylib$"sa
6✔
805
    else
806
        # On Linux and FreeBSD, libraries look like `libnettle.so.6.3.0`
807
        dlregex = r"^(.*?)\.so((?:\.[\d]+)*)$"sa
×
808
    end
809

810
    m = match(dlregex, basename(path))
27✔
811
    if m === nothing
27✔
812
        throw(ArgumentError("Invalid dynamic library path '$path'"))
10✔
813
    end
814

815
    # Extract name and version
816
    name = m.captures[1]
17✔
817
    version = m.captures[2]
17✔
818
    if version === nothing || isempty(version)
32✔
819
        version = nothing
×
820
    else
821
        version = VersionNumber(strip(version, '.'))
11✔
822
    end
823
    return name, version
17✔
824
end
825

826
# Adapter for `AbstractString`
827
function parse_dl_name_version(path::AbstractString, os::AbstractString)
×
828
    return parse_dl_name_version(string(path)::String, string(os)::String)
×
829
end
830

831
"""
832
    detect_libgfortran_version()
833

834
Inspects the current Julia process to determine the libgfortran version this Julia is
835
linked against (if any).
836
"""
837
function detect_libgfortran_version()
3✔
838
    libgfortran_paths = filter!(x -> occursin("libgfortran", x), Libdl.dllist())
102✔
839
    if isempty(libgfortran_paths)
3✔
840
        # One day, I hope to not be linking against libgfortran in base Julia
841
        return nothing
×
842
    end
843
    libgfortran_path = first(libgfortran_paths)
3✔
844

845
    name, version = parse_dl_name_version(libgfortran_path, os())
6✔
846
    if version === nothing
3✔
847
        # Even though we complain about this, we allow it to continue in the hopes that
848
        # we shall march on to a BRIGHTER TOMORROW.  One in which we are not shackled
849
        # by the constraints of libgfortran compiler ABIs upon our precious programming
850
        # languages; one where the mistakes of yesterday are mere memories and not
851
        # continual maintenance burdens upon the children of the dawn; one where numeric
852
        # code may be cleanly implemented in a modern language and not bestowed onto the
853
        # next generation by grizzled ancients, documented only with a faded yellow
854
        # sticky note that bears a hastily-scribbled "good luck".
855
        @warn("Unable to determine libgfortran version from '$(libgfortran_path)'")
×
856
    end
857
    return version
3✔
858
end
859

860
"""
861
    detect_libstdcxx_version(max_minor_version::Int=30)
862

863
Inspects the currently running Julia process to find out what version of libstdc++
864
it is linked against (if any).  `max_minor_version` is the latest version in the
865
3.4 series of GLIBCXX where the search is performed.
866
"""
867
function detect_libstdcxx_version(max_minor_version::Int=30)
80✔
868
    libstdcxx_paths = filter!(x -> occursin("libstdc++", x), Libdl.dllist())
3,055✔
869
    if isempty(libstdcxx_paths)
79✔
870
        # This can happen if we were built by clang, so we don't link against
871
        # libstdc++ at all.
872
        return nothing
×
873
    end
874

875
    # Brute-force our way through GLIBCXX_* symbols to discover which version we're linked against
876
    hdl = Libdl.dlopen(first(libstdcxx_paths))::Ptr{Cvoid}
79✔
877
    # Try all GLIBCXX versions down to GCC v4.8:
878
    # https://gcc.gnu.org/onlinedocs/libstdc++/manual/abi.html
879
    for minor_version in max_minor_version:-1:18
158✔
880
        if Libdl.dlsym(hdl, "GLIBCXX_3.4.$(minor_version)"; throw_error=false) !== nothing
79✔
881
            Libdl.dlclose(hdl)
79✔
882
            return VersionNumber("3.4.$(minor_version)")
79✔
883
        end
884
    end
×
885
    Libdl.dlclose(hdl)
×
886
    return nothing
×
887
end
888

889
"""
890
    detect_cxxstring_abi()
891

892
Inspects the currently running Julia process to see what version of the C++11 string ABI
893
it was compiled with (this is only relevant if compiled with `g++`; `clang` has no
894
incompatibilities yet, bless its heart).  In reality, this actually checks for symbols
895
within LLVM, but that is close enough for our purposes, as you can't mix configurations
896
between Julia and LLVM; they must match.
897
"""
898
function detect_cxxstring_abi()
1✔
899
    # First, if we're not linked against libstdc++, then early-exit because this doesn't matter.
900
    libstdcxx_paths = filter!(x -> occursin("libstdc++", x), Libdl.dllist())
34✔
901
    if isempty(libstdcxx_paths)
1✔
902
        # We were probably built by `clang`; we don't link against `libstdc++`` at all.
903
        return nothing
×
904
    end
905

906
    function open_libllvm(f::Function)
1✔
907
        for lib_name in (Base.libllvm_name, "libLLVM", "LLVM", "libLLVMSupport")
1✔
908
            hdl = Libdl.dlopen_e(lib_name)
2✔
909
            if hdl != C_NULL
1✔
910
                try
1✔
911
                    return f(hdl)
1✔
912
                finally
913
                    Libdl.dlclose(hdl)
1✔
914
                end
915
            end
916
        end
×
917
        error("Unable to open libLLVM!")
×
918
    end
919

920
    return open_libllvm() do hdl
1✔
921
        # Check for llvm::sys::getProcessTriple(), first without cxx11 tag:
922
        if Libdl.dlsym_e(hdl, "_ZN4llvm3sys16getProcessTripleEv") != C_NULL
1✔
923
            return "cxx03"
×
924
        elseif Libdl.dlsym_e(hdl, "_ZN4llvm3sys16getProcessTripleB5cxx11Ev") != C_NULL
2✔
925
            return "cxx11"
1✔
926
        else
927
            @warn("Unable to find llvm::sys::getProcessTriple() in libLLVM!")
×
928
            return nothing
×
929
        end
930
    end
931
end
932

933
"""
934
    host_triplet()
935

936
Build host triplet out of `Sys.MACHINE` and various introspective utilities that
937
detect compiler ABI values such as `libgfortran_version`, `libstdcxx_version` and
938
`cxxstring_abi`.  We do this without using any `Platform` tech as it must run before
939
we have much of that built.
940
"""
941
function host_triplet()
78✔
942
    str = Base.BUILD_TRIPLET
×
943

944
    if !occursin("-libgfortran", str)
78✔
945
        libgfortran_version = detect_libgfortran_version()
×
946
        if libgfortran_version !== nothing
×
947
            str = string(str, "-libgfortran", libgfortran_version.major)
×
948
        end
949
    end
950

951
    if !occursin("-cxx", str)
78✔
952
        cxxstring_abi = detect_cxxstring_abi()
×
953
        if cxxstring_abi !== nothing
×
954
            str = string(str, "-", cxxstring_abi)
×
955
        end
956
    end
957

958
    if !occursin("-libstdcxx", str)
78✔
959
        libstdcxx_version = detect_libstdcxx_version()
78✔
960
        if libstdcxx_version !== nothing
78✔
961
            str = string(str, "-libstdcxx", libstdcxx_version.patch)
78✔
962
        end
963
    end
964

965
    # Add on julia_version extended tag
966
    if !occursin("-julia_version+", str)
78✔
967
        str = string(str, "-julia_version+", VersionNumber(VERSION.major, VERSION.minor, VERSION.patch))
78✔
968
    end
969
    return str
78✔
970
end
971

972
"""
973
    HostPlatform()
974

975
Return the `Platform` object that corresponds to the current host system, with all
976
relevant comparison strategies set to host platform mode.  This is equivalent to:
977

978
    HostPlatform(parse(Platform, Base.BinaryPlatforms.host_triplet()))
979
"""
980
function HostPlatform()
19✔
981
    return HostPlatform(parse(Platform, host_triplet()))::Platform
78✔
982
end
983

984
"""
985
    platforms_match(a::AbstractPlatform, b::AbstractPlatform)
986

987
Return `true` if `a` and `b` are matching platforms, where matching is determined by
988
comparing all keys contained within the platform objects, and if both objects contain
989
entries for that key, they must match.  Comparison, by default, is performed using
990
the `==` operator, however this can be overridden on a key-by-key basis by adding
991
"comparison strategies" through `set_compare_strategy!(platform, key, func)`.
992

993
Note that as the comparison strategy is set on the `Platform` object, and not globally,
994
a custom comparison strategy is first looked for within the `a` object, then if none
995
is found, it is looked for in the `b` object.  Finally, if none is found in either, the
996
default of `==(ak, bk)` is used.  We throw an error if custom comparison strategies are
997
used on both `a` and `b` and they are not the same custom comparison.
998

999
The reserved tags `os_version` and `libstdcxx_version` use this mechanism to provide
1000
bounded version constraints, where an artifact can specify that it was built using APIs
1001
only available in macOS `v"10.11"` and later, or an artifact can state that it requires
1002
a libstdc++ that is at least `v"3.4.22"`, etc...
1003
"""
1004
function platforms_match(a::AbstractPlatform, b::AbstractPlatform)
494✔
1005
    for k in union(keys(tags(a)::Dict{String,String}), keys(tags(b)::Dict{String,String}))
988✔
1006
        ak = get(tags(a), k, nothing)
2,839✔
1007
        bk = get(tags(b), k, nothing)
3,240✔
1008

1009
        # Only continue if both `ak` and `bk` are not `nothing`
1010
        if ak === nothing || bk === nothing
2,839✔
1011
            continue
×
1012
        end
1013

1014
        a_comp = get_compare_strategy(a, k)
969✔
1015
        b_comp = get_compare_strategy(b, k)
959✔
1016

1017
        # Throw an error if `a` and `b` have both set non-default comparison strategies for `k`
1018
        # and they're not the same strategy.
1019
        if a_comp !== compare_default && b_comp !== compare_default && a_comp !== b_comp
949✔
1020
            throw(ArgumentError("Cannot compare Platform objects with two different non-default comparison strategies for the same key \"$(k)\""))
×
1021
        end
1022

1023
        # Select the custom comparator, if we have one.
1024
        comparator = a_comp
×
1025
        if b_comp !== compare_default
949✔
1026
            comparator = b_comp
×
1027
        end
1028

1029
        # Call the comparator, passing in which objects requested this comparison (one, the other, or both)
1030
        # For some comparators this doesn't matter, but for non-symmetrical comparisons, it does.
1031
        if !(comparator(ak, bk, a_comp === comparator, b_comp === comparator)::Bool)
949✔
1032
            return false
336✔
1033
        end
1034
    end
2,748✔
1035
    return true
158✔
1036
end
1037

1038
function platforms_match(a::String, b::AbstractPlatform)
27✔
1039
    return platforms_match(parse(Platform, a), b)
27✔
1040
end
1041
function platforms_match(a::AbstractPlatform, b::String)
27✔
1042
    return platforms_match(a, parse(Platform, b))
27✔
1043
end
1044
platforms_match(a::String, b::String) = platforms_match(parse(Platform, a), parse(Platform, b))
×
1045

1046
# Adapters for AbstractString backedge avoidance
1047
platforms_match(a::AbstractString, b::AbstractPlatform) = platforms_match(string(a)::String, b)
×
1048
platforms_match(a::AbstractPlatform, b::AbstractString) = platforms_match(a, string(b)::String)
×
1049
platforms_match(a::AbstractString, b::AbstractString) = platforms_match(string(a)::String, string(b)::String)
×
1050

1051

1052
"""
1053
    select_platform(download_info::Dict, platform::AbstractPlatform = HostPlatform())
1054

1055
Given a `download_info` dictionary mapping platforms to some value, choose
1056
the value whose key best matches `platform`, returning `nothing` if no matches
1057
can be found.
1058

1059
Platform attributes such as architecture, libc, calling ABI, etc... must all
1060
match exactly, however attributes such as compiler ABI can have wildcards
1061
within them such as `nothing` which matches any version of GCC.
1062
"""
1063
function select_platform(download_info::Dict, platform::AbstractPlatform = HostPlatform())
30✔
1064
    ps = collect(filter(p -> platforms_match(p, platform), keys(download_info)))
366✔
1065

1066
    if isempty(ps)
30✔
1067
        return nothing
4✔
1068
    end
1069

1070
    # At this point, we may have multiple possibilities.  We now engage a multi-
1071
    # stage selection algorithm, where we first sort the matches by how complete
1072
    # the match is, e.g. preferring matches where the intersection of tags is
1073
    # equal to the union of the tags:
1074
    function match_loss(a, b)
26✔
1075
        a_tags = Set(keys(tags(a)))
26✔
1076
        b_tags = Set(keys(tags(b)))
26✔
1077
        return length(union(a_tags, b_tags)) - length(intersect(a_tags, b_tags))
26✔
1078
    end
1079

1080
    # We prefer these better matches, and secondarily reverse-sort by triplet so
1081
    # as to generally choose the latest release (e.g. a `libgfortran5` tarball
1082
    # over a `libgfortran3` tarball).
1083
    sort!(ps, lt = (a, b) -> begin
39✔
1084
        loss_a = match_loss(a, platform)
13✔
1085
        loss_b = match_loss(b, platform)
13✔
1086
        if loss_a != loss_b
13✔
1087
            return loss_a < loss_b
9✔
1088
        end
1089
        return triplet(a) > triplet(b)
4✔
1090
    end)
1091

1092
    # @invokelatest here to not get invalidated by new defs of `==(::Function, ::Function)`
1093
    return @invokelatest getindex(download_info, first(ps))
26✔
1094
end
1095

1096
# precompiles to reduce latency (see https://github.com/JuliaLang/julia/pull/43990#issuecomment-1025692379)
1097
Dict{Platform,String}()[HostPlatform()] = ""
1098
Platform("x86_64", "linux", Dict{String,Any}(); validate_strict=true)
1099
Platform("x86_64", "linux", Dict{String,String}(); validate_strict=false)  # called this way from Artifacts.unpack_platform
1100

1101
end # module
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

© 2026 Coveralls, Inc