From aa2ea48bf2743cea81266738e7efb4555dac2b8c Mon Sep 17 00:00:00 2001 From: Jordan Hubbard Date: Thu, 1 Jan 2026 14:51:51 -0400 Subject: [PATCH] feat: add FreeBSD release builds (#832) * feat: add FreeBSD release builds Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * chore: allow manual release dispatch Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * fix: stabilize release workflow on fork Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * fix: clean zig download artifact Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * fix: use valid zig target for freebsd arm Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * fix: disable freebsd arm release build Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * fix: switch freebsd build to pure go Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * fix: skip release publishing on forks Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * fix: satisfy golangci-lint for release PR --------- Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 9 ++- .goreleaser.yml | 17 +++++ cmd/bd/activity.go | 6 +- cmd/bd/doctor/migration.go | 4 +- cmd/bd/hooks.go | 2 +- cmd/bd/jira.go | 46 ++++++------ cmd/bd/list.go | 74 ++++++++++--------- cmd/bd/preflight.go | 4 +- .../migrations/022_drop_edge_columns.go | 1 + internal/syncbranch/integrity.go | 10 +-- internal/ui/pager.go | 2 +- scripts/install.sh | 32 +++++++- 12 files changed, 133 insertions(+), 74 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e05c60f..153fa922 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,7 @@ on: push: tags: - 'v*' + workflow_dispatch: concurrency: group: release-${{ github.ref }} @@ -36,7 +37,9 @@ jobs: with: distribution: goreleaser version: '~> v2' - args: release --clean + args: > + release --clean + ${{ github.repository != 'steveyegge/beads' && '--skip=publish --skip=announce' || '' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Windows code signing (optional - signing is skipped if not set) @@ -46,7 +49,7 @@ jobs: publish-pypi: runs-on: ubuntu-latest needs: goreleaser - if: always() # Run even if goreleaser fails + if: ${{ always() && github.repository == 'steveyegge/beads' }} steps: - name: Checkout uses: actions/checkout@v6 @@ -75,6 +78,7 @@ jobs: publish-npm: runs-on: ubuntu-latest needs: goreleaser + if: ${{ github.repository == 'steveyegge/beads' }} permissions: contents: read id-token: write # Required for npm provenance/trusted publishing @@ -101,6 +105,7 @@ jobs: update-homebrew: runs-on: ubuntu-latest needs: goreleaser + if: ${{ github.repository == 'steveyegge/beads' }} steps: - name: Checkout uses: actions/checkout@v6 diff --git a/.goreleaser.yml b/.goreleaser.yml index 3f97adab..0b8336ad 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -99,6 +99,23 @@ builds: # Requires WINDOWS_SIGNING_CERT_PFX_BASE64 and WINDOWS_SIGNING_CERT_PASSWORD secrets - ./scripts/sign-windows.sh "{{ .Path }}" + - id: bd-freebsd-amd64 + main: ./cmd/bd + binary: bd + env: + - CGO_ENABLED=0 + goos: + - freebsd + goarch: + - amd64 + ldflags: + - -s -w + - -X main.Version={{.Version}} + - -X main.Build={{.ShortCommit}} + - -X main.Commit={{.Commit}} + - -X main.Branch={{.Branch}} + + archives: - id: bd-archive format: tar.gz diff --git a/cmd/bd/activity.go b/cmd/bd/activity.go index a7814af1..1c604978 100644 --- a/cmd/bd/activity.go +++ b/cmd/bd/activity.go @@ -504,7 +504,7 @@ func discoverRigDaemons() []rigDaemon { // Similar to routing.resolveRedirect but simplified for activity use. func resolveBeadsRedirect(beadsDir string) string { redirectFile := filepath.Join(beadsDir, "redirect") - data, err := os.ReadFile(redirectFile) + data, err := os.ReadFile(redirectFile) // #nosec G304 - redirects are trusted within beads rig paths if err != nil { return beadsDir } @@ -729,7 +729,9 @@ func runTownActivityFollow(sinceTime time.Time) { func closeDaemons(daemons []rigDaemon) { for _, d := range daemons { if d.client != nil { - d.client.Close() + if err := d.client.Close(); err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to close daemon client: %v\n", err) + } } } } diff --git a/cmd/bd/doctor/migration.go b/cmd/bd/doctor/migration.go index 9914a408..92cc9424 100644 --- a/cmd/bd/doctor/migration.go +++ b/cmd/bd/doctor/migration.go @@ -56,7 +56,7 @@ func DetectPendingMigrations(path string) []PendingMigration { } // Check for missing sync-branch config (sync migration) - if needsSyncMigration(beadsDir, path) { + if needsSyncMigration(path) { pending = append(pending, PendingMigration{ Name: "sync", Description: "Configure sync branch for multi-clone setup", @@ -206,7 +206,7 @@ func needsTombstonesMigration(beadsDir string) bool { } // needsSyncMigration checks if sync-branch should be configured -func needsSyncMigration(beadsDir, repoPath string) bool { +func needsSyncMigration(repoPath string) bool { // Check if already configured if syncbranch.GetFromYAML() != "" { return false diff --git a/cmd/bd/hooks.go b/cmd/bd/hooks.go index a811abf5..7d9a8971 100644 --- a/cmd/bd/hooks.go +++ b/cmd/bd/hooks.go @@ -782,7 +782,7 @@ func runPrepareCommitMsgHook(args []string) int { } // Write back - if err := os.WriteFile(msgFile, []byte(sb.String()), 0644); err != nil { // #nosec G306 + if err := os.WriteFile(msgFile, []byte(sb.String()), 0600); err != nil { // Restrict permissions per gosec G306 fmt.Fprintf(os.Stderr, "Warning: could not write commit message: %v\n", err) } diff --git a/cmd/bd/jira.go b/cmd/bd/jira.go index 1490a042..23644b14 100644 --- a/cmd/bd/jira.go +++ b/cmd/bd/jira.go @@ -23,22 +23,22 @@ import ( // JiraSyncStats tracks statistics for a Jira sync operation. type JiraSyncStats struct { - Pulled int `json:"pulled"` - Pushed int `json:"pushed"` - Created int `json:"created"` - Updated int `json:"updated"` - Skipped int `json:"skipped"` - Errors int `json:"errors"` + Pulled int `json:"pulled"` + Pushed int `json:"pushed"` + Created int `json:"created"` + Updated int `json:"updated"` + Skipped int `json:"skipped"` + Errors int `json:"errors"` Conflicts int `json:"conflicts"` } // JiraSyncResult represents the result of a Jira sync operation. type JiraSyncResult struct { - Success bool `json:"success"` - Stats JiraSyncStats `json:"stats"` - LastSync string `json:"last_sync,omitempty"` - Error string `json:"error,omitempty"` - Warnings []string `json:"warnings,omitempty"` + Success bool `json:"success"` + Stats JiraSyncStats `json:"stats"` + LastSync string `json:"last_sync,omitempty"` + Error string `json:"error,omitempty"` + Warnings []string `json:"warnings,omitempty"` } var jiraCmd = &cobra.Command{ @@ -288,13 +288,13 @@ var jiraStatusCmd = &cobra.Command{ if jsonOutput { outputJSON(map[string]interface{}{ - "configured": configured, - "jira_url": jiraURL, - "jira_project": jiraProject, - "last_sync": lastSync, - "total_issues": len(allIssues), - "with_jira_ref": withJiraRef, - "pending_push": pendingPush, + "configured": configured, + "jira_url": jiraURL, + "jira_project": jiraProject, + "last_sync": lastSync, + "total_issues": len(allIssues), + "with_jira_ref": withJiraRef, + "pending_push": pendingPush, }) return } @@ -610,9 +610,9 @@ Looked in: %v`, name, locations) // JiraConflict represents a conflict between local and Jira versions. type JiraConflict struct { - IssueID string - LocalUpdated time.Time - JiraUpdated time.Time + IssueID string + LocalUpdated time.Time + JiraUpdated time.Time JiraExternalRef string } @@ -854,7 +854,9 @@ func fetchJiraIssueTimestamp(ctx context.Context, jiraKey string) (time.Time, er if err != nil { return zero, fmt.Errorf("failed to fetch issue %s: %w", jiraKey, err) } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) diff --git a/cmd/bd/list.go b/cmd/bd/list.go index 1446f85b..dbba778b 100644 --- a/cmd/bd/list.go +++ b/cmd/bd/list.go @@ -385,12 +385,12 @@ var listCmd = &cobra.Command{ longFormat, _ := cmd.Flags().GetBool("long") sortBy, _ := cmd.Flags().GetString("sort") reverse, _ := cmd.Flags().GetBool("reverse") - + // Pattern matching flags titleContains, _ := cmd.Flags().GetString("title-contains") descContains, _ := cmd.Flags().GetString("desc-contains") notesContains, _ := cmd.Flags().GetString("notes-contains") - + // Date range flags createdAfter, _ := cmd.Flags().GetString("created-after") createdBefore, _ := cmd.Flags().GetString("created-before") @@ -398,12 +398,12 @@ var listCmd = &cobra.Command{ updatedBefore, _ := cmd.Flags().GetString("updated-before") closedAfter, _ := cmd.Flags().GetString("closed-after") closedBefore, _ := cmd.Flags().GetString("closed-before") - + // Empty/null check flags emptyDesc, _ := cmd.Flags().GetBool("empty-description") noAssignee, _ := cmd.Flags().GetBool("no-assignee") noLabels, _ := cmd.Flags().GetBool("no-labels") - + // Priority range flags priorityMinStr, _ := cmd.Flags().GetString("priority-min") priorityMaxStr, _ := cmd.Flags().GetString("priority-max") @@ -509,7 +509,7 @@ var listCmd = &cobra.Command{ filter.IDs = ids } } - + // Pattern matching if titleContains != "" { filter.TitleContains = titleContains @@ -520,7 +520,7 @@ var listCmd = &cobra.Command{ if notesContains != "" { filter.NotesContains = notesContains } - + // Date ranges if createdAfter != "" { t, err := parseTimeFlag(createdAfter) @@ -570,7 +570,7 @@ var listCmd = &cobra.Command{ } filter.ClosedBefore = &t } - + // Empty/null checks if emptyDesc { filter.EmptyDescription = true @@ -581,7 +581,7 @@ var listCmd = &cobra.Command{ if noLabels { filter.NoLabels = true } - + // Priority ranges if cmd.Flags().Changed("priority-min") { priorityMin, err := validation.ValidatePriority(priorityMinStr) @@ -640,7 +640,7 @@ var listCmd = &cobra.Command{ } } - // If daemon is running, use RPC + // If daemon is running, use RPC if daemonClient != nil { listArgs := &rpc.ListArgs{ Status: status, @@ -665,17 +665,17 @@ var listCmd = &cobra.Command{ } // Forward title search via Query field (searches title/description/id) if titleSearch != "" { - listArgs.Query = titleSearch + listArgs.Query = titleSearch } - if len(filter.IDs) > 0 { - listArgs.IDs = filter.IDs - } - + if len(filter.IDs) > 0 { + listArgs.IDs = filter.IDs + } + // Pattern matching listArgs.TitleContains = titleContains listArgs.DescriptionContains = descContains listArgs.NotesContains = notesContains - + // Date ranges if filter.CreatedAfter != nil { listArgs.CreatedAfter = filter.CreatedAfter.Format(time.RFC3339) @@ -695,12 +695,12 @@ var listCmd = &cobra.Command{ if filter.ClosedBefore != nil { listArgs.ClosedBefore = filter.ClosedBefore.Format(time.RFC3339) } - + // Empty/null checks listArgs.EmptyDescription = filter.EmptyDescription listArgs.NoAssignee = filter.NoAssignee listArgs.NoLabels = filter.NoLabels - + // Priority range listArgs.PriorityMin = filter.PriorityMin listArgs.PriorityMax = filter.PriorityMax @@ -721,7 +721,7 @@ var listCmd = &cobra.Command{ } } - resp, err := daemonClient.List(listArgs) + resp, err := daemonClient.List(listArgs) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) @@ -774,7 +774,9 @@ var listCmd = &cobra.Command{ // Output with pager support if err := ui.ToPager(buf.String(), ui.PagerOptions{NoPager: noPager}); err != nil { - fmt.Fprint(os.Stdout, buf.String()) + if _, writeErr := fmt.Fprint(os.Stdout, buf.String()); writeErr != nil { + fmt.Fprintf(os.Stderr, "Error writing output: %v\n", writeErr) + } } // Show truncation hint if we hit the limit (GH#788) @@ -788,21 +790,21 @@ var listCmd = &cobra.Command{ // ctx already created above for staleness check issues, err := store.SearchIssues(ctx, "", filter) if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) } - // If no issues found, check if git has issues and auto-import - if len(issues) == 0 { - if checkAndAutoImport(ctx, store) { - // Re-run the query after import - issues, err = store.SearchIssues(ctx, "", filter) - if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) + // If no issues found, check if git has issues and auto-import + if len(issues) == 0 { + if checkAndAutoImport(ctx, store) { + // Re-run the query after import + issues, err = store.SearchIssues(ctx, "", filter) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } } } - } // Apply sorting sortIssues(issues, sortBy, reverse) @@ -899,7 +901,9 @@ var listCmd = &cobra.Command{ // Output with pager support if err := ui.ToPager(buf.String(), ui.PagerOptions{NoPager: noPager}); err != nil { - fmt.Fprint(os.Stdout, buf.String()) + if _, writeErr := fmt.Fprint(os.Stdout, buf.String()); writeErr != nil { + fmt.Fprintf(os.Stderr, "Error writing output: %v\n", writeErr) + } } // Show truncation hint if we hit the limit (GH#788) @@ -927,12 +931,12 @@ func init() { listCmd.Flags().Bool("long", false, "Show detailed multi-line output for each issue") listCmd.Flags().String("sort", "", "Sort by field: priority, created, updated, closed, status, id, title, type, assignee") listCmd.Flags().BoolP("reverse", "r", false, "Reverse sort order") - + // Pattern matching listCmd.Flags().String("title-contains", "", "Filter by title substring (case-insensitive)") listCmd.Flags().String("desc-contains", "", "Filter by description substring (case-insensitive)") listCmd.Flags().String("notes-contains", "", "Filter by notes substring (case-insensitive)") - + // Date ranges listCmd.Flags().String("created-after", "", "Filter issues created after date (YYYY-MM-DD or RFC3339)") listCmd.Flags().String("created-before", "", "Filter issues created before date (YYYY-MM-DD or RFC3339)") @@ -940,12 +944,12 @@ func init() { listCmd.Flags().String("updated-before", "", "Filter issues updated before date (YYYY-MM-DD or RFC3339)") listCmd.Flags().String("closed-after", "", "Filter issues closed after date (YYYY-MM-DD or RFC3339)") listCmd.Flags().String("closed-before", "", "Filter issues closed before date (YYYY-MM-DD or RFC3339)") - + // Empty/null checks listCmd.Flags().Bool("empty-description", false, "Filter issues with empty or missing description") listCmd.Flags().Bool("no-assignee", false, "Filter issues with no assignee") listCmd.Flags().Bool("no-labels", false, "Filter issues with no labels") - + // Priority ranges listCmd.Flags().String("priority-min", "", "Filter by minimum priority (inclusive, 0-4 or P0-P4)") listCmd.Flags().String("priority-max", "", "Filter by maximum priority (inclusive, 0-4 or P0-P4)") diff --git a/cmd/bd/preflight.go b/cmd/bd/preflight.go index 995ed251..ae058449 100644 --- a/cmd/bd/preflight.go +++ b/cmd/bd/preflight.go @@ -139,7 +139,9 @@ func runChecks(jsonOutput bool) { } enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") - enc.Encode(result) + if err := enc.Encode(result); err != nil { + fmt.Fprintf(os.Stderr, "Error encoding preflight result: %v\n", err) + } } else { // Human-readable output for _, r := range results { diff --git a/internal/storage/sqlite/migrations/022_drop_edge_columns.go b/internal/storage/sqlite/migrations/022_drop_edge_columns.go index 1fd7d89d..f9dbf844 100644 --- a/internal/storage/sqlite/migrations/022_drop_edge_columns.go +++ b/internal/storage/sqlite/migrations/022_drop_edge_columns.go @@ -185,6 +185,7 @@ func MigrateDropEdgeColumns(db *sql.DB) error { // Copy data from old table to new table (excluding deprecated columns) // NOTE: We use fmt.Sprintf here (not db.Exec parameters) because we're interpolating // column names/expressions, not values. db.Exec parameters only work for VALUES. + // #nosec G201 - expressions are column names, not user input copySQL := fmt.Sprintf(` INSERT INTO issues_new ( id, content_hash, title, description, design, acceptance_criteria, diff --git a/internal/syncbranch/integrity.go b/internal/syncbranch/integrity.go index 1aa06273..d688a8fe 100644 --- a/internal/syncbranch/integrity.go +++ b/internal/syncbranch/integrity.go @@ -79,7 +79,7 @@ func CheckForcePush(ctx context.Context, store storage.Storage, repoRoot, syncBr status.Remote = getRemoteForBranch(ctx, worktreePath, syncBranch) // Fetch from remote to get latest state - fetchCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "fetch", status.Remote, syncBranch) + fetchCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "fetch", status.Remote, syncBranch) // #nosec G204 - repoRoot/syncBranch are validated git inputs fetchOutput, err := fetchCmd.CombinedOutput() if err != nil { // Check if remote branch doesn't exist @@ -92,7 +92,7 @@ func CheckForcePush(ctx context.Context, store storage.Storage, repoRoot, syncBr // Get current remote SHA remoteRef := fmt.Sprintf("%s/%s", status.Remote, syncBranch) - revParseCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "rev-parse", remoteRef) + revParseCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "rev-parse", remoteRef) // #nosec G204 - remoteRef constructed from trusted config revParseOutput, err := revParseCmd.Output() if err != nil { return nil, fmt.Errorf("failed to get remote SHA: %w", err) @@ -107,7 +107,7 @@ func CheckForcePush(ctx context.Context, store storage.Storage, repoRoot, syncBr // Check if stored SHA is an ancestor of current remote SHA // This means remote was updated normally (fast-forward) - isAncestorCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "merge-base", "--is-ancestor", storedSHA, status.CurrentRemoteSHA) + isAncestorCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "merge-base", "--is-ancestor", storedSHA, status.CurrentRemoteSHA) // #nosec G204 - args derive from git SHAs we validated earlier if isAncestorCmd.Run() == nil { // Stored SHA is ancestor - normal update, no force-push status.Message = "Remote sync branch updated normally (fast-forward)" @@ -146,12 +146,12 @@ func UpdateStoredRemoteSHA(ctx context.Context, store storage.Storage, repoRoot, // Get current remote SHA remoteRef := fmt.Sprintf("%s/%s", remote, syncBranch) - revParseCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "rev-parse", remoteRef) + revParseCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "rev-parse", remoteRef) // #nosec G204 - remoteRef is internal config revParseOutput, err := revParseCmd.Output() if err != nil { // Remote branch might not exist yet (first push) // Try local branch instead - revParseCmd = exec.CommandContext(ctx, "git", "-C", repoRoot, "rev-parse", syncBranch) + revParseCmd = exec.CommandContext(ctx, "git", "-C", repoRoot, "rev-parse", syncBranch) // #nosec G204 - branch name from config revParseOutput, err = revParseCmd.Output() if err != nil { return fmt.Errorf("failed to get sync branch SHA: %w", err) diff --git a/internal/ui/pager.go b/internal/ui/pager.go index b6be46aa..aa4330c5 100644 --- a/internal/ui/pager.go +++ b/internal/ui/pager.go @@ -100,7 +100,7 @@ func ToPager(content string, opts PagerOptions) error { return nil } - cmd := exec.Command(parts[0], parts[1:]...) + cmd := exec.Command(parts[0], parts[1:]...) // #nosec G204 - pager command is user-configurable by design cmd.Stdin = strings.NewReader(content) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/scripts/install.sh b/scripts/install.sh index 5c0ad2fe..1c9dbe84 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -34,6 +34,17 @@ log_error() { echo -e "${RED}Error:${NC} $1" >&2 } +release_has_asset() { + local release_json=$1 + local asset_name=$2 + + if echo "$release_json" | grep -Fq "\"name\": \"$asset_name\""; then + return 0 + fi + + return 1 +} + # Re-sign binary for macOS to avoid slow Gatekeeper checks # See: https://github.com/steveyegge/beads/issues/466 resign_for_macos() { @@ -70,6 +81,9 @@ detect_platform() { Linux) os="linux" ;; + FreeBSD) + os="freebsd" + ;; *) log_error "Unsupported operating system: $(uname -s)" exit 1 @@ -83,6 +97,9 @@ detect_platform() { aarch64|arm64) arch="arm64" ;; + armv7*|armv6*|armhf|arm) + arch="arm" + ;; *) log_error "Unsupported architecture: $(uname -m)" exit 1 @@ -104,16 +121,19 @@ install_from_release() { log_info "Fetching latest release..." local latest_url="https://api.github.com/repos/steveyegge/beads/releases/latest" local version - + local release_json + if command -v curl &> /dev/null; then - version=$(curl -fsSL "$latest_url" | grep '"tag_name"' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/') + release_json=$(curl -fsSL "$latest_url") elif command -v wget &> /dev/null; then - version=$(wget -qO- "$latest_url" | grep '"tag_name"' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/') + release_json=$(wget -qO- "$latest_url") else log_error "Neither curl nor wget found. Please install one of them." return 1 fi + version=$(echo "$release_json" | grep '"tag_name"' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/') + if [ -z "$version" ]; then log_error "Failed to fetch latest version" return 1 @@ -124,6 +144,12 @@ install_from_release() { # Download URL local archive_name="beads_${version#v}_${platform}.tar.gz" local download_url="https://github.com/steveyegge/beads/releases/download/${version}/${archive_name}" + + if ! release_has_asset "$release_json" "$archive_name"; then + log_warning "No prebuilt archive available for platform ${platform}. Falling back to source installation methods." + rm -rf "$tmp_dir" + return 1 + fi log_info "Downloading $archive_name..."