Pull request: all: openbsd support

Updates #2439.

Squashed commit of the following:

commit 3ff109e43751132d77500256c8869938680ac281
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Jun 3 20:46:17 2021 +0300

    all: imp code, docs

commit 512ee6d78cfee511f429d09c8366bb7dd8019aa8
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Jun 3 20:06:41 2021 +0300

    all: openbsd support
This commit is contained in:
Ainar Garipov 2021-06-03 21:04:13 +03:00
parent 084564e6b7
commit 3b87478470
13 changed files with 124 additions and 72 deletions

View file

@ -15,6 +15,7 @@ and this project adheres to
### Added ### Added
- Experimental OpenBSD support for AMD64 and 64-bit ARM CPUs ([#2439]).
- Support for custom port in DNS-over-HTTPS profiles for Apple's devices - Support for custom port in DNS-over-HTTPS profiles for Apple's devices
([#3172]). ([#3172]).
- `darwin/arm64` support ([#2443]). - `darwin/arm64` support ([#2443]).
@ -52,6 +53,7 @@ released by then.
- Go 1.15 support. - Go 1.15 support.
[#2439]: https://github.com/AdguardTeam/AdGuardHome/issues/2439
[#2441]: https://github.com/AdguardTeam/AdGuardHome/issues/2441 [#2441]: https://github.com/AdguardTeam/AdGuardHome/issues/2441
[#2443]: https://github.com/AdguardTeam/AdGuardHome/issues/2443 [#2443]: https://github.com/AdguardTeam/AdGuardHome/issues/2443
[#3136]: https://github.com/AdguardTeam/AdGuardHome/issues/3136 [#3136]: https://github.com/AdguardTeam/AdGuardHome/issues/3136

View file

@ -349,6 +349,13 @@ on GitHub and most other Markdown renderers. -->
* `snake_case`, not `camelCase` for variables. `kebab-case` for filenames. * `snake_case`, not `camelCase` for variables. `kebab-case` for filenames.
* Start scripts with the following sections in the following order:
1. Shebang.
1. Some initial documentation (optional).
1. Verbosity level parsing (optional).
1. `set` options.
* UPPERCASE names for external exported variables, lowercase for local, * UPPERCASE names for external exported variables, lowercase for local,
unexported ones. unexported ones.

View file

@ -116,6 +116,7 @@ go-check: go-tools go-lint go-test
go-os-check: go-os-check:
env GOOS='darwin' "$(GO.MACRO)" vet ./internal/... env GOOS='darwin' "$(GO.MACRO)" vet ./internal/...
env GOOS='freebsd' "$(GO.MACRO)" vet ./internal/... env GOOS='freebsd' "$(GO.MACRO)" vet ./internal/...
env GOOS='openbsd' "$(GO.MACRO)" vet ./internal/...
env GOOS='linux' "$(GO.MACRO)" vet ./internal/... env GOOS='linux' "$(GO.MACRO)" vet ./internal/...
env GOOS='windows' "$(GO.MACRO)" vet ./internal/... env GOOS='windows' "$(GO.MACRO)" vet ./internal/...

View file

@ -289,6 +289,8 @@ curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/s
* macOS ARM: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_arm64.zip) * macOS ARM: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_arm64.zip)
* FreeBSD: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_amd64.tar.gz), [32-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_386.tar.gz) * FreeBSD: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_amd64.tar.gz), [32-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_386.tar.gz)
* FreeBSD ARM: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_arm64.tar.gz), [32-bit ARMv5](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv5.tar.gz), [32-bit ARMv6](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv6.tar.gz), [32-bit ARMv7](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv7.tar.gz) * FreeBSD ARM: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_arm64.tar.gz), [32-bit ARMv5](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv5.tar.gz), [32-bit ARMv6](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv6.tar.gz), [32-bit ARMv7](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv7.tar.gz)
* OpenBSD: (coming soon)
* OpenBSD ARM: (coming soon)
* Edge channel builds * Edge channel builds
* Linux: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_amd64.tar.gz), [32-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_386.tar.gz) * Linux: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_amd64.tar.gz), [32-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_386.tar.gz)
@ -299,6 +301,8 @@ curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/s
* macOS ARM: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_darwin_arm64.zip) * macOS ARM: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_darwin_arm64.zip)
* FreeBSD: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_amd64.tar.gz), [32-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_386.tar.gz) * FreeBSD: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_amd64.tar.gz), [32-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_386.tar.gz)
* FreeBSD ARM: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_arm64.tar.gz), [32-bit ARMv5](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_armv5.tar.gz), [32-bit ARMv6](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_armv6.tar.gz), [32-bit ARMv7](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_armv7.tar.gz) * FreeBSD ARM: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_arm64.tar.gz), [32-bit ARMv5](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_armv5.tar.gz), [32-bit ARMv6](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_armv6.tar.gz), [32-bit ARMv7](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_armv7.tar.gz)
* OpenBSD: [64-bit (experimental)](https://static.adguard.com/adguardhome/edge/AdGuardHome_openbsd_amd64.tar.gz)
* OpenBSD ARM: [64-bit (experimental)](https://static.adguard.com/adguardhome/edge/AdGuardHome_openbsd_arm64.tar.gz)
<a id="reporting-issues"></a> <a id="reporting-issues"></a>

View file

@ -5,18 +5,28 @@ import (
"fmt" "fmt"
"os/exec" "os/exec"
"syscall" "syscall"
"github.com/AdguardTeam/golibs/errors"
) )
// ErrUnsupported is returned when the functionality is unsupported on the
// current operating system.
//
// TODO(a.garipov): Make a structured error and use it everywhere instead of
// a bunch of fmt.Errorf and all that.
const ErrUnsupported errors.Error = "unsupported"
// CanBindPrivilegedPorts checks if current process can bind to privileged // CanBindPrivilegedPorts checks if current process can bind to privileged
// ports. // ports.
func CanBindPrivilegedPorts() (can bool, err error) { func CanBindPrivilegedPorts() (can bool, err error) {
return canBindPrivilegedPorts() return canBindPrivilegedPorts()
} }
// SetRlimit sets user-specified limit of how many fd's we can use // SetRlimit sets user-specified limit of how many fd's we can use.
// https://github.com/AdguardTeam/AdGuardHome/internal/issues/659. //
func SetRlimit(val uint) { // See https://github.com/AdguardTeam/AdGuardHome/internal/issues/659.
setRlimit(val) func SetRlimit(val uint64) (err error) {
return setRlimit(val)
} }
// HaveAdminRights checks if the current user has root (administrator) rights. // HaveAdminRights checks if the current user has root (administrator) rights.

View file

@ -7,22 +7,18 @@ package aghos
import ( import (
"os" "os"
"syscall" "syscall"
"github.com/AdguardTeam/golibs/log"
) )
func canBindPrivilegedPorts() (can bool, err error) { func canBindPrivilegedPorts() (can bool, err error) {
return HaveAdminRights() return HaveAdminRights()
} }
func setRlimit(val uint) { func setRlimit(val uint64) (err error) {
var rlim syscall.Rlimit var rlim syscall.Rlimit
rlim.Max = uint64(val) rlim.Max = val
rlim.Cur = uint64(val) rlim.Cur = val
err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim)
if err != nil { return syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim)
log.Error("Setrlimit() failed: %v", err)
}
} }
func haveAdminRights() (bool, error) { func haveAdminRights() (bool, error) {

View file

@ -7,22 +7,18 @@ package aghos
import ( import (
"os" "os"
"syscall" "syscall"
"github.com/AdguardTeam/golibs/log"
) )
func canBindPrivilegedPorts() (can bool, err error) { func canBindPrivilegedPorts() (can bool, err error) {
return HaveAdminRights() return HaveAdminRights()
} }
func setRlimit(val uint) { func setRlimit(val uint64) (err error) {
var rlim syscall.Rlimit var rlim syscall.Rlimit
rlim.Max = int64(val) rlim.Max = int64(val)
rlim.Cur = int64(val) rlim.Cur = int64(val)
err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim)
if err != nil { return syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim)
log.Error("Setrlimit() failed: %v", err)
}
} }
func haveAdminRights() (bool, error) { func haveAdminRights() (bool, error) {

View file

@ -11,7 +11,6 @@ import (
"strings" "strings"
"syscall" "syscall"
"github.com/AdguardTeam/golibs/log"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
@ -23,14 +22,12 @@ func canBindPrivilegedPorts() (can bool, err error) {
return cnbs == 1 || adm, err return cnbs == 1 || adm, err
} }
func setRlimit(val uint) { func setRlimit(val uint64) (err error) {
var rlim syscall.Rlimit var rlim syscall.Rlimit
rlim.Max = uint64(val) rlim.Max = val
rlim.Cur = uint64(val) rlim.Cur = val
err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim)
if err != nil { return syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim)
log.Error("Setrlimit() failed: %v", err)
}
} }
func haveAdminRights() (bool, error) { func haveAdminRights() (bool, error) {

View file

@ -15,7 +15,8 @@ func canBindPrivilegedPorts() (can bool, err error) {
return HaveAdminRights() return HaveAdminRights()
} }
func setRlimit(val uint) { func setRlimit(val uint64) (err error) {
return ErrUnsupported
} }
func haveAdminRights() (bool, error) { func haveAdminRights() (bool, error) {

View file

@ -54,7 +54,7 @@ type configuration struct {
AuthBlockMin uint `yaml:"block_auth_min"` AuthBlockMin uint `yaml:"block_auth_min"`
ProxyURL string `yaml:"http_proxy"` // Proxy address for our HTTP client ProxyURL string `yaml:"http_proxy"` // Proxy address for our HTTP client
Language string `yaml:"language"` // two-letter ISO 639-1 language code Language string `yaml:"language"` // two-letter ISO 639-1 language code
RlimitNoFile uint `yaml:"rlimit_nofile"` // Maximum number of opened fd's per process (0: default) RlimitNoFile uint64 `yaml:"rlimit_nofile"` // Maximum number of opened fd's per process (0: default)
DebugPProf bool `yaml:"debug_pprof"` // Enable pprof HTTP server on port 6060 DebugPProf bool `yaml:"debug_pprof"` // Enable pprof HTTP server on port 6060
// TTL for a web session (in hours) // TTL for a web session (in hours)

View file

@ -115,7 +115,15 @@ func Main(clientBuildFS fs.FS) {
}() }()
if args.serviceControlAction != "" { if args.serviceControlAction != "" {
// TODO(a.garipov): github.com/kardianos/service doesn't seem to
// support OpenBSD currently. Either patch it to do so or make
// our own implementation of the service.System interface.
if runtime.GOOS == "openbsd" {
log.Fatal("service actions are not supported on openbsd")
}
handleServiceControlAction(args, clientBuildFS) handleServiceControlAction(args, clientBuildFS)
return return
} }
@ -175,7 +183,7 @@ func setupContext(args options) {
Context.mux = http.NewServeMux() Context.mux = http.NewServeMux()
} }
func setupConfig(args options) { func setupConfig(args options) (err error) {
config.DHCP.WorkDir = Context.workDir config.DHCP.WorkDir = Context.workDir
config.DHCP.HTTPRegister = httpRegister config.DHCP.HTTPRegister = httpRegister
config.DHCP.ConfigModified = onConfigModified config.DHCP.ConfigModified = onConfigModified
@ -186,7 +194,7 @@ func setupConfig(args options) {
// now which assume that the DHCP server can be nil despite this // now which assume that the DHCP server can be nil despite this
// condition. Inspect them and perhaps rewrite them to use // condition. Inspect them and perhaps rewrite them to use
// Enabled() instead. // Enabled() instead.
log.Fatalf("can't initialize dhcp module") return fmt.Errorf("initing dhcp: %w", err)
} }
Context.updater = updater.NewUpdater(&updater.Config{ Context.updater = updater.NewUpdater(&updater.Config{
@ -208,9 +216,11 @@ func setupConfig(args options) {
Context.clients.Init(config.Clients, Context.dhcpServer, Context.etcHosts) Context.clients.Init(config.Clients, Context.dhcpServer, Context.etcHosts)
config.Clients = nil config.Clients = nil
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") && if config.RlimitNoFile != 0 {
config.RlimitNoFile != 0 { err = aghos.SetRlimit(config.RlimitNoFile)
aghos.SetRlimit(config.RlimitNoFile) if err != nil && !errors.Is(err, aghos.ErrUnsupported) {
return fmt.Errorf("setting rlimit: %w", err)
}
} }
// override bind host/port from the console // override bind host/port from the console
@ -223,6 +233,8 @@ func setupConfig(args options) {
if len(args.pidFile) != 0 && writePIDFile(args.pidFile) { if len(args.pidFile) != 0 && writePIDFile(args.pidFile) {
Context.pidFileName = args.pidFile Context.pidFileName = args.pidFile
} }
return nil
} }
func initWeb(args options, clientBuildFS fs.FS) (web *Web, err error) { func initWeb(args options, clientBuildFS fs.FS) (web *Web, err error) {
@ -266,8 +278,16 @@ func initWeb(args options, clientBuildFS fs.FS) (web *Web, err error) {
return web, nil return web, nil
} }
func fatalOnError(err error) {
if err != nil {
log.Fatal(err)
}
}
// run performs configurating and starts AdGuard Home. // run performs configurating and starts AdGuard Home.
func run(args options, clientBuildFS fs.FS) { func run(args options, clientBuildFS fs.FS) {
var err error
// configure config filename // configure config filename
initConfigFilename(args) initConfigFilename(args)
@ -294,14 +314,13 @@ func run(args options, clientBuildFS fs.FS) {
// but also avoid relying on automatic Go init() function // but also avoid relying on automatic Go init() function
filtering.InitModule() filtering.InitModule()
setupConfig(args) err = setupConfig(args)
fatalOnError(err)
if !Context.firstRun { if !Context.firstRun {
// Save the updated config // Save the updated config
err := config.write() err = config.write()
if err != nil { fatalOnError(err)
log.Fatal(err)
}
if config.DebugPProf { if config.DebugPProf {
mux := http.NewServeMux() mux := http.NewServeMux()
@ -318,7 +337,7 @@ func run(args options, clientBuildFS fs.FS) {
} }
} }
err := os.MkdirAll(Context.getDataDir(), 0o755) err = os.MkdirAll(Context.getDataDir(), 0o755)
if err != nil { if err != nil {
log.Fatalf("Cannot create DNS data dir at %s: %s", Context.getDataDir(), err) log.Fatalf("Cannot create DNS data dir at %s: %s", Context.getDataDir(), err)
} }
@ -352,20 +371,14 @@ func run(args options, clientBuildFS fs.FS) {
} }
Context.web, err = initWeb(args, clientBuildFS) Context.web, err = initWeb(args, clientBuildFS)
if err != nil { fatalOnError(err)
log.Fatal(err)
}
Context.subnetDetector, err = aghnet.NewSubnetDetector() Context.subnetDetector, err = aghnet.NewSubnetDetector()
if err != nil { fatalOnError(err)
log.Fatal(err)
}
if !Context.firstRun { if !Context.firstRun {
err = initDNSServer() err = initDNSServer()
if err != nil { fatalOnError(err)
log.Fatalf("%s", err)
}
Context.tls.Start() Context.tls.Start()
Context.etcHosts.Start() Context.etcHosts.Start()
@ -374,7 +387,7 @@ func run(args options, clientBuildFS fs.FS) {
serr := startDNSServer() serr := startDNSServer()
if serr != nil { if serr != nil {
closeDNSServer() closeDNSServer()
log.Fatal(serr) fatalOnError(serr)
} }
}() }()

View file

@ -2,6 +2,10 @@
# AdGuard Home Installation Script # AdGuard Home Installation Script
# Exit the script if a pipeline fails (-e), prevent accidental filename
# expansion (-f), and consider undefined variables as errors (-u).
set -e -f -u
# Function log is an echo wrapper that writes to stderr if the caller # Function log is an echo wrapper that writes to stderr if the caller
# requested verbosity level greater than 0. Otherwise, it does nothing. # requested verbosity level greater than 0. Otherwise, it does nothing.
log() { log() {
@ -54,7 +58,7 @@ check_required() {
required="curl" required="curl"
case "$os" case "$os"
in in
('freebsd'|'linux') ('freebsd'|'linux'|'openbsd')
required="$required $required_unix" required="$required $required_unix"
;; ;;
('darwin') ('darwin')
@ -63,7 +67,7 @@ check_required() {
(*) (*)
# Generally shouldn't happen, since the OS has already been # Generally shouldn't happen, since the OS has already been
# validated. # validated.
error_exit "unsupported operating system: $os" error_exit "unsupported operating system: '$os'"
;; ;;
esac esac
@ -173,14 +177,17 @@ set_os() {
os="$( uname -s )" os="$( uname -s )"
case "$os" case "$os"
in in
('Linux') ('Darwin')
os='linux' os='darwin'
;; ;;
('FreeBSD') ('FreeBSD')
os='freebsd' os='freebsd'
;; ;;
('Darwin') ('Linux')
os='darwin' os='linux'
;;
('OpenBSD')
os='openbsd'
;; ;;
esac esac
fi fi
@ -188,7 +195,7 @@ set_os() {
# Validate. # Validate.
case "$os" case "$os"
in in
('darwin'|'freebsd'|'linux') ('darwin'|'freebsd'|'linux'|'openbsd')
# All right, go on. # All right, go on.
;; ;;
(*) (*)
@ -310,12 +317,30 @@ fix_freebsd() {
fi fi
} }
# Function set_sudo_cmd sets the appropriate command to run a command under
# superuser privileges.
set_sudo_cmd() {
case "$os"
in
('openbsd')
sudo_cmd='doas'
;;
('darwin'|'freebsd'|'linux')
# Go on and use the default, sudo.
;;
(*)
error_exit "unsupported operating system: '$os'"
;;
esac
}
# Function configure sets the script's configuration. # Function configure sets the script's configuration.
configure() { configure() {
set_channel set_channel
set_os set_os
set_cpu set_cpu
fix_darwin fix_darwin
set_sudo_cmd
check_out_dir check_out_dir
pkg_name="AdGuardHome_${os}_${cpu}.${pkg_ext}" pkg_name="AdGuardHome_${os}_${cpu}.${pkg_ext}"
@ -335,8 +360,7 @@ is_root() {
return 0 return 0
fi fi
# TODO(a.garipov): On OpenBSD, use doas. if is_command "$sudo_cmd"
if is_command sudo
then then
log 'note that AdGuard Home requires root privileges to install using this script' log 'note that AdGuard Home requires root privileges to install using this script'
@ -358,24 +382,25 @@ rerun_with_root() {
'https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh' 'https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh'
readonly script_url readonly script_url
flags='' r='-R'
if [ "$reinstall" -eq '1' ] if [ "$reinstall" -eq '1' ]
then then
flags="${flags} -r" r='-r'
fi fi
u='-U'
if [ "$uninstall" -eq '1' ] if [ "$uninstall" -eq '1' ]
then then
flags="${flags} -u" u='-u'
fi fi
v='-V'
if [ "$verbose" -eq '1' ] if [ "$verbose" -eq '1' ]
then then
flags="${flags} -v" v='-v'
fi fi
opts="-c $channel -C $cpu -O $os -o $out_dir $flags" readonly r u v
readonly opts
log 'restarting with root privileges' log 'restarting with root privileges'
@ -384,7 +409,7 @@ rerun_with_root() {
# following shell to execute to prevent it from getting an empty input # following shell to execute to prevent it from getting an empty input
# and exiting with a zero code in that case. # and exiting with a zero code in that case.
{ curl -L -S -s "$script_url" || echo 'exit 1'; }\ { curl -L -S -s "$script_url" || echo 'exit 1'; }\
| sudo sh -s -- $opts | $sudo_cmd sh -s -- -c "$channel" -C "$cpu" -O "$os" -o "$out_dir" "$r" "$u" "$v"
# Exit the script. Since if the code of the previous pipeline is # Exit the script. Since if the code of the previous pipeline is
# non-zero, the execution won't reach this point thanks to set -e, exit # non-zero, the execution won't reach this point thanks to set -e, exit
@ -504,10 +529,6 @@ install_service() {
# Entrypoint # Entrypoint
# Exit the script if a pipeline fails (-e), prevent accidental filename
# expansion (-f), and consider undefined variables as errors (-u).
set -e -f -u
# Set default values of configuration variables. # Set default values of configuration variables.
channel='release' channel='release'
reinstall='0' reinstall='0'
@ -517,6 +538,8 @@ cpu=''
os='' os=''
out_dir='/opt' out_dir='/opt'
pkg_ext='tar.gz' pkg_ext='tar.gz'
sudo_cmd='sudo'
parse_opts "$@" parse_opts "$@"
echo 'starting AdGuard Home installation script' echo 'starting AdGuard Home installation script'
@ -541,4 +564,4 @@ install_service
echo "\ echo "\
AdGuard Home is now installed and running AdGuard Home is now installed and running
you can control the service status with the following commands: you can control the service status with the following commands:
sudo ${agh_dir}/AdGuardHome -s start|stop|restart|status|install|uninstall" $sudo_cmd ${agh_dir}/AdGuardHome -s start|stop|restart|status|install|uninstall"

View file

@ -156,6 +156,8 @@ linux mips64 0 softfloat 0
linux mips64le 0 softfloat 0 linux mips64le 0 softfloat 0
linux mipsle 0 softfloat 0 linux mipsle 0 softfloat 0
linux ppc64le 0 0 0 linux ppc64le 0 0 0
openbsd amd64 0 0 0
openbsd arm64 0 0 0
windows 386 0 0 0 windows 386 0 0 0
windows amd64 0 0 0" windows amd64 0 0 0"
readonly platforms readonly platforms