diff --git a/.gitignore b/.gitignore index 017bd56..7735519 100644 --- a/.gitignore +++ b/.gitignore @@ -94,4 +94,6 @@ rnd/ keys_backup/ -vps.txt \ No newline at end of file +vps.txt + +bin-linux/ \ No newline at end of file diff --git a/Makefile b/Makefile index 7e4029d..b0f5911 100644 --- a/Makefile +++ b/Makefile @@ -90,6 +90,7 @@ VERSION := 0.100.0 COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ) LDFLAGS := -X 'main.version=$(VERSION)' -X 'main.commit=$(COMMIT)' -X 'main.date=$(DATE)' +LDFLAGS_LINUX := -s -w $(LDFLAGS) # Build targets build: deps @@ -108,14 +109,14 @@ build: deps build-linux: deps @echo "Cross-compiling all binaries for linux/amd64 (version=$(VERSION))..." @mkdir -p bin-linux - GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin-linux/identity ./cmd/identity - GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin-linux/orama-node ./cmd/node - GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin-linux/orama cmd/cli/main.go - GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin-linux/rqlite-mcp ./cmd/rqlite-mcp - GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS) -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildVersion=$(VERSION)' -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildCommit=$(COMMIT)' -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildTime=$(DATE)'" -o bin-linux/gateway ./cmd/gateway - GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin-linux/orama-cli ./cmd/cli - @echo "Installing Olric for linux/amd64..." - GOOS=linux GOARCH=amd64 GOBIN=$(CURDIR)/bin-linux go install github.com/olric-data/olric/cmd/olric-server@v0.7.0 + GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS_LINUX)" -trimpath -o bin-linux/identity ./cmd/identity + GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS_LINUX)" -trimpath -o bin-linux/orama-node ./cmd/node + GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS_LINUX)" -trimpath -o bin-linux/orama cmd/cli/main.go + GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS_LINUX)" -trimpath -o bin-linux/rqlite-mcp ./cmd/rqlite-mcp + GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS_LINUX) -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildVersion=$(VERSION)' -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildCommit=$(COMMIT)' -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildTime=$(DATE)'" -trimpath -o bin-linux/gateway ./cmd/gateway + GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS_LINUX)" -trimpath -o bin-linux/orama-cli ./cmd/cli + @echo "Building Olric for linux/amd64..." + GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -trimpath -o bin-linux/olric-server github.com/olric-data/olric/cmd/olric-server @echo "✓ All Linux binaries built in bin-linux/" @echo "" @echo "Next steps:" @@ -125,31 +126,11 @@ build-linux: deps # Build CoreDNS with rqlite plugin for Linux build-linux-coredns: - @echo "Building CoreDNS with rqlite plugin for linux/amd64..." - @rm -rf /tmp/coredns-build-linux - @mkdir -p /tmp/coredns-build-linux - git clone --depth 1 --branch v1.12.0 https://github.com/coredns/coredns.git /tmp/coredns-build-linux - @mkdir -p /tmp/coredns-build-linux/plugin/rqlite - @cp pkg/coredns/rqlite/*.go /tmp/coredns-build-linux/plugin/rqlite/ - @echo 'metadata:metadata\ncancel:cancel\ntls:tls\nreload:reload\nnsid:nsid\nbufsize:bufsize\nroot:root\nbind:bind\ndebug:debug\ntrace:trace\nready:ready\nhealth:health\npprof:pprof\nprometheus:metrics\nerrors:errors\nlog:log\ndnstap:dnstap\nlocal:local\ndns64:dns64\nacl:acl\nany:any\nchaos:chaos\nloadbalance:loadbalance\ncache:cache\nrewrite:rewrite\nheader:header\ndnssec:dnssec\nautopath:autopath\nminimal:minimal\ntemplate:template\ntransfer:transfer\nhosts:hosts\nfile:file\nauto:auto\nsecondary:secondary\nloop:loop\nforward:forward\ngrpc:grpc\nerratic:erratic\nwhoami:whoami\non:github.com/coredns/caddy/onevent\nsign:sign\nview:view\nrqlite:rqlite' > /tmp/coredns-build-linux/plugin.cfg - cd /tmp/coredns-build-linux && go get github.com/miekg/dns@latest && go get go.uber.org/zap@latest && go mod tidy && go generate && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o coredns - @cp /tmp/coredns-build-linux/coredns bin-linux/coredns - @rm -rf /tmp/coredns-build-linux - @echo "✓ CoreDNS built: bin-linux/coredns" + @bash scripts/build-linux-coredns.sh # Build Caddy with orama DNS module for Linux build-linux-caddy: - @echo "Building Caddy with orama DNS module for linux/amd64..." - @which xcaddy > /dev/null 2>&1 || (echo "Installing xcaddy..." && go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest) - @rm -rf /tmp/caddy-build-linux - @mkdir -p /tmp/caddy-build-linux/caddy-dns-orama - @# Generate the module source - @echo 'module github.com/DeBrosOfficial/caddy-dns-orama\n\ngo 1.22\n\nrequire (\n\tgithub.com/caddyserver/caddy/v2 v2.10.2\n\tgithub.com/libdns/libdns v1.1.0\n)' > /tmp/caddy-build-linux/caddy-dns-orama/go.mod - @cp pkg/caddy/dns/orama/provider.go /tmp/caddy-build-linux/caddy-dns-orama/ 2>/dev/null || true - cd /tmp/caddy-build-linux/caddy-dns-orama && go mod tidy - GOOS=linux GOARCH=amd64 xcaddy build v2.10.2 --with github.com/DeBrosOfficial/caddy-dns-orama=/tmp/caddy-build-linux/caddy-dns-orama --output bin-linux/caddy - @rm -rf /tmp/caddy-build-linux - @echo "✓ Caddy built: bin-linux/caddy" + @bash scripts/build-linux-caddy.sh # Build everything for Linux (all binaries + CoreDNS + Caddy) build-linux-all: build-linux build-linux-coredns build-linux-caddy diff --git a/docs/DEV_DEPLOY.md b/docs/DEV_DEPLOY.md index 5b5980c..fab369d 100644 --- a/docs/DEV_DEPLOY.md +++ b/docs/DEV_DEPLOY.md @@ -79,27 +79,18 @@ Cross-compile everything locally and skip all Go compilation on the VPS. This is make build-linux-all # Outputs everything to bin-linux/ -# 2. Generate a source archive (still needed for configs, templates, etc.) +# 2. Generate a single deploy archive (source + pre-built binaries) ./scripts/generate-source-archive.sh +# Creates: /tmp/network-source.tar.gz (includes bin-linux/ if present) -# 3. Copy everything to the VPS -sshpass -p '' scp -o StrictHostKeyChecking=no bin-linux/orama ubuntu@:/tmp/orama +# 3. Copy the single archive to the VPS sshpass -p '' scp -o StrictHostKeyChecking=no /tmp/network-source.tar.gz ubuntu@:/tmp/ -# 4. On the VPS: extract source, install CLI, and copy pre-built binaries -ssh ubuntu@ -sudo rm -rf /home/debros/src && sudo mkdir -p /home/debros/src -sudo tar xzf /tmp/network-source.tar.gz -C /home/debros/src -sudo chown -R debros:debros /home/debros/src -sudo mv /tmp/orama /usr/local/bin/orama && sudo chmod +x /usr/local/bin/orama +# 4. Extract and install everything on the VPS +sshpass -p '' ssh -o StrictHostKeyChecking=no ubuntu@ \ + 'sudo bash -s' < scripts/extract-deploy.sh -# 5. Copy pre-built binaries to their expected locations -# (from your local machine) -sshpass -p '' scp -o StrictHostKeyChecking=no bin-linux/orama-node bin-linux/gateway bin-linux/identity bin-linux/rqlite-mcp bin-linux/olric-server ubuntu@:/home/debros/bin/ -sshpass -p '' scp -o StrictHostKeyChecking=no bin-linux/coredns ubuntu@:/usr/local/bin/coredns -sshpass -p '' scp -o StrictHostKeyChecking=no bin-linux/caddy ubuntu@:/usr/bin/caddy - -# 6. Install/upgrade with --pre-built (skips ALL Go compilation on VPS) +# 5. Install/upgrade with --pre-built (skips ALL Go compilation on VPS) sudo orama install --no-pull --pre-built --vps-ip ... # or sudo orama upgrade --no-pull --pre-built --restart diff --git a/scripts/build-linux-caddy.sh b/scripts/build-linux-caddy.sh new file mode 100755 index 0000000..5a00ab4 --- /dev/null +++ b/scripts/build-linux-caddy.sh @@ -0,0 +1,223 @@ +#!/bin/bash +# Build Caddy with orama DNS module for linux/amd64 +# Outputs to bin-linux/caddy +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +OUTPUT_DIR="$PROJECT_ROOT/bin-linux" +BUILD_DIR="/tmp/caddy-build-linux" +MODULE_DIR="$BUILD_DIR/caddy-dns-orama" + +mkdir -p "$OUTPUT_DIR" + +# Ensure xcaddy is installed +if ! command -v xcaddy &> /dev/null; then + echo "Installing xcaddy..." + go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest +fi + +# Clean up previous build +rm -rf "$BUILD_DIR" +mkdir -p "$MODULE_DIR" + +# Write go.mod +cat > "$MODULE_DIR/go.mod" << 'GOMOD' +module github.com/DeBrosOfficial/caddy-dns-orama + +go 1.22 + +require ( + github.com/caddyserver/caddy/v2 v2.10.2 + github.com/libdns/libdns v1.1.0 +) +GOMOD + +# Write provider.go (the orama DNS provider for ACME DNS-01 challenges) +cat > "$MODULE_DIR/provider.go" << 'PROVIDERGO' +// Package orama implements a DNS provider for Caddy that uses the Orama Network +// gateway's internal ACME API for DNS-01 challenge validation. +package orama + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/libdns/libdns" +) + +func init() { + caddy.RegisterModule(Provider{}) +} + +// Provider wraps the Orama DNS provider for Caddy. +type Provider struct { + // Endpoint is the URL of the Orama gateway's ACME API + // Default: http://localhost:6001/v1/internal/acme + Endpoint string `json:"endpoint,omitempty"` +} + +// CaddyModule returns the Caddy module information. +func (Provider) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "dns.providers.orama", + New: func() caddy.Module { return new(Provider) }, + } +} + +// Provision sets up the module. +func (p *Provider) Provision(ctx caddy.Context) error { + if p.Endpoint == "" { + p.Endpoint = "http://localhost:6001/v1/internal/acme" + } + return nil +} + +// UnmarshalCaddyfile parses the Caddyfile configuration. +func (p *Provider) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + for d.NextBlock(0) { + switch d.Val() { + case "endpoint": + if !d.NextArg() { + return d.ArgErr() + } + p.Endpoint = d.Val() + default: + return d.Errf("unrecognized option: %s", d.Val()) + } + } + } + return nil +} + +// AppendRecords adds records to the zone. For ACME, this presents the challenge. +func (p *Provider) AppendRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) { + var added []libdns.Record + + for _, rec := range records { + rr := rec.RR() + if rr.Type != "TXT" { + continue + } + + fqdn := rr.Name + "." + zone + + payload := map[string]string{ + "fqdn": fqdn, + "value": rr.Data, + } + + body, err := json.Marshal(payload) + if err != nil { + return added, fmt.Errorf("failed to marshal request: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", p.Endpoint+"/present", bytes.NewReader(body)) + if err != nil { + return added, fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return added, fmt.Errorf("failed to present challenge: %w", err) + } + resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return added, fmt.Errorf("present failed with status %d", resp.StatusCode) + } + + added = append(added, rec) + } + + return added, nil +} + +// DeleteRecords removes records from the zone. For ACME, this cleans up the challenge. +func (p *Provider) DeleteRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) { + var deleted []libdns.Record + + for _, rec := range records { + rr := rec.RR() + if rr.Type != "TXT" { + continue + } + + fqdn := rr.Name + "." + zone + + payload := map[string]string{ + "fqdn": fqdn, + "value": rr.Data, + } + + body, err := json.Marshal(payload) + if err != nil { + return deleted, fmt.Errorf("failed to marshal request: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", p.Endpoint+"/cleanup", bytes.NewReader(body)) + if err != nil { + return deleted, fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return deleted, fmt.Errorf("failed to cleanup challenge: %w", err) + } + resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return deleted, fmt.Errorf("cleanup failed with status %d", resp.StatusCode) + } + + deleted = append(deleted, rec) + } + + return deleted, nil +} + +// GetRecords returns the records in the zone. Not used for ACME. +func (p *Provider) GetRecords(ctx context.Context, zone string) ([]libdns.Record, error) { + return nil, nil +} + +// SetRecords sets the records in the zone. Not used for ACME. +func (p *Provider) SetRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) { + return nil, nil +} + +// Interface guards +var ( + _ caddy.Module = (*Provider)(nil) + _ caddy.Provisioner = (*Provider)(nil) + _ caddyfile.Unmarshaler = (*Provider)(nil) + _ libdns.RecordAppender = (*Provider)(nil) + _ libdns.RecordDeleter = (*Provider)(nil) + _ libdns.RecordGetter = (*Provider)(nil) + _ libdns.RecordSetter = (*Provider)(nil) +) +PROVIDERGO + +# Run go mod tidy +cd "$MODULE_DIR" && go mod tidy + +# Build with xcaddy +echo "Building Caddy binary..." +GOOS=linux GOARCH=amd64 xcaddy build v2.10.2 \ + --with "github.com/DeBrosOfficial/caddy-dns-orama=$MODULE_DIR" \ + --output "$OUTPUT_DIR/caddy" + +# Cleanup +rm -rf "$BUILD_DIR" +echo "✓ Caddy built: bin-linux/caddy" diff --git a/scripts/build-linux-coredns.sh b/scripts/build-linux-coredns.sh new file mode 100755 index 0000000..e3d36ab --- /dev/null +++ b/scripts/build-linux-coredns.sh @@ -0,0 +1,91 @@ +#!/bin/bash +# Build CoreDNS with rqlite plugin for linux/amd64 +# Outputs to bin-linux/coredns +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +OUTPUT_DIR="$PROJECT_ROOT/bin-linux" +BUILD_DIR="/tmp/coredns-build-linux" + +mkdir -p "$OUTPUT_DIR" + +# Clean up previous build +rm -rf "$BUILD_DIR" + +# Clone CoreDNS +echo "Cloning CoreDNS v1.12.0..." +git clone --depth 1 --branch v1.12.0 https://github.com/coredns/coredns.git "$BUILD_DIR" + +# Copy rqlite plugin +echo "Copying rqlite plugin..." +mkdir -p "$BUILD_DIR/plugin/rqlite" +cp "$PROJECT_ROOT/pkg/coredns/rqlite/"*.go "$BUILD_DIR/plugin/rqlite/" + +# Write plugin.cfg +cat > "$BUILD_DIR/plugin.cfg" << 'EOF' +metadata:metadata +cancel:cancel +tls:tls +reload:reload +nsid:nsid +bufsize:bufsize +root:root +bind:bind +debug:debug +trace:trace +ready:ready +health:health +pprof:pprof +prometheus:metrics +errors:errors +log:log +dnstap:dnstap +local:local +dns64:dns64 +acl:acl +any:any +chaos:chaos +loadbalance:loadbalance +cache:cache +rewrite:rewrite +header:header +dnssec:dnssec +autopath:autopath +minimal:minimal +template:template +transfer:transfer +hosts:hosts +file:file +auto:auto +secondary:secondary +loop:loop +forward:forward +grpc:grpc +erratic:erratic +whoami:whoami +on:github.com/coredns/caddy/onevent +sign:sign +view:view +rqlite:rqlite +EOF + +# Build +cd "$BUILD_DIR" +echo "Adding dependencies..." +go get github.com/miekg/dns@latest +go get go.uber.org/zap@latest +go mod tidy + +echo "Generating plugin code..." +go generate + +echo "Building CoreDNS binary..." +GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath -o coredns + +# Copy output +cp "$BUILD_DIR/coredns" "$OUTPUT_DIR/coredns" + +# Cleanup +rm -rf "$BUILD_DIR" +echo "✓ CoreDNS built: bin-linux/coredns" diff --git a/scripts/extract-deploy.sh b/scripts/extract-deploy.sh new file mode 100755 index 0000000..81ac928 --- /dev/null +++ b/scripts/extract-deploy.sh @@ -0,0 +1,77 @@ +#!/bin/bash +# Extracts /tmp/network-source.tar.gz on a VPS and places everything correctly. +# Run as root on the target VPS. +# +# What it does: +# 1. Extracts source to /home/debros/src/ +# 2. Installs CLI to /usr/local/bin/orama +# 3. If bin-linux/ is in the archive (pre-built), copies binaries to their locations: +# - orama-node, gateway, identity, rqlite-mcp, olric-server → /home/debros/bin/ +# - coredns → /usr/local/bin/coredns +# - caddy → /usr/bin/caddy +# +# Usage: sudo bash /home/debros/src/scripts/extract-deploy.sh +# (or pipe via SSH: ssh root@host 'bash -s' < scripts/extract-deploy.sh) + +set -e + +ARCHIVE="/tmp/network-source.tar.gz" +SRC_DIR="/home/debros/src" +BIN_DIR="/home/debros/bin" + +if [ ! -f "$ARCHIVE" ]; then + echo "Error: $ARCHIVE not found" + exit 1 +fi + +# Ensure debros user exists (orama install also creates it, but we need it now for chown) +if ! id -u debros &>/dev/null; then + echo "Creating 'debros' user..." + useradd -m -s /bin/bash debros +fi + +echo "Extracting source..." +rm -rf "$SRC_DIR" +mkdir -p "$SRC_DIR" "$BIN_DIR" +tar xzf "$ARCHIVE" -C "$SRC_DIR" +id -u debros &>/dev/null && chown -R debros:debros "$SRC_DIR" || true + +# Install CLI +if [ -f "$SRC_DIR/bin-linux/orama" ]; then + cp "$SRC_DIR/bin-linux/orama" /usr/local/bin/orama + chmod +x /usr/local/bin/orama + echo " ✓ CLI installed: /usr/local/bin/orama" +fi + +# Place pre-built binaries if present +if [ -d "$SRC_DIR/bin-linux" ]; then + echo "Installing pre-built binaries..." + + for bin in orama-node gateway identity rqlite-mcp olric-server orama; do + if [ -f "$SRC_DIR/bin-linux/$bin" ]; then + cp "$SRC_DIR/bin-linux/$bin" "$BIN_DIR/$bin" + chmod +x "$BIN_DIR/$bin" + echo " ✓ $bin → $BIN_DIR/$bin" + fi + done + + if [ -f "$SRC_DIR/bin-linux/coredns" ]; then + cp "$SRC_DIR/bin-linux/coredns" /usr/local/bin/coredns + chmod +x /usr/local/bin/coredns + echo " ✓ coredns → /usr/local/bin/coredns" + fi + + if [ -f "$SRC_DIR/bin-linux/caddy" ]; then + cp "$SRC_DIR/bin-linux/caddy" /usr/bin/caddy + chmod +x /usr/bin/caddy + echo " ✓ caddy → /usr/bin/caddy" + fi + + id -u debros &>/dev/null && chown -R debros:debros "$BIN_DIR" || true + echo "All binaries installed." +else + echo "No pre-built binaries in archive (bin-linux/ not found)." + echo "Install CLI manually: sudo mv /tmp/orama /usr/local/bin/orama" +fi + +echo "Done. Ready for: sudo orama install --no-pull --pre-built ..." diff --git a/scripts/generate-source-archive.sh b/scripts/generate-source-archive.sh index d716a28..d06c34e 100755 --- a/scripts/generate-source-archive.sh +++ b/scripts/generate-source-archive.sh @@ -2,6 +2,9 @@ # Generates a tarball of the current codebase for deployment # Output: /tmp/network-source.tar.gz # +# If bin-linux/ exists (from make build-linux-all), it is included in the archive. +# On the VPS, use scripts/extract-deploy.sh to extract source + place binaries. +# # Usage: ./scripts/generate-source-archive.sh set -e @@ -10,13 +13,20 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" OUTPUT="/tmp/network-source.tar.gz" -echo "Generating source archive..." - cd "$PROJECT_ROOT" # Remove root-level binaries before archiving (they'll be rebuilt on VPS) rm -f gateway cli node orama-cli-linux 2>/dev/null +# Check if pre-built binaries exist +if [ -d "bin-linux" ] && [ "$(ls -A bin-linux 2>/dev/null)" ]; then + echo "Generating source archive (with pre-built binaries)..." + EXCLUDE_BIN="" +else + echo "Generating source archive (source only, no bin-linux/)..." + EXCLUDE_BIN="--exclude=bin-linux/" +fi + tar czf "$OUTPUT" \ --exclude='.git' \ --exclude='node_modules' \ @@ -29,7 +39,12 @@ tar czf "$OUTPUT" \ --exclude='testdata/' \ --exclude='examples/' \ --exclude='*.tar.gz' \ + $EXCLUDE_BIN \ . echo "Archive created: $OUTPUT" echo "Size: $(du -h $OUTPUT | cut -f1)" + +if [ -d "bin-linux" ] && [ "$(ls -A bin-linux 2>/dev/null)" ]; then + echo "Includes pre-built binaries: $(ls bin-linux/ | tr '\n' ' ')" +fi