Skip to content

Commit b21dc73

Browse files
authored
feat: Added download-sources command (#67)
Adds new command `azldev advanced download-sources` that downloads source tarballs from a lookaside cache using a Fedora-format `sources` file.
1 parent 0590d38 commit b21dc73

File tree

9 files changed

+590
-7
lines changed

9 files changed

+590
-7
lines changed

docs/user/reference/cli/azldev_advanced.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/user/reference/cli/azldev_advanced_download-sources.md

Lines changed: 78 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/app/azldev/cmds/advanced/advanced.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package advanced
55

66
import (
77
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev"
8+
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/cmds/downloadsources"
89
"github.com/spf13/cobra"
910
)
1011

@@ -23,6 +24,7 @@ output but fully supported.`,
2324
}
2425

2526
app.AddTopLevelCommand(cmd)
27+
downloadsources.OnAppInit(app, cmd)
2628
mcpOnAppInit(app, cmd)
2729
mockOnAppInit(app, cmd)
2830
wgetOnAppInit(app, cmd)
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package downloadsources
5+
6+
import (
7+
"errors"
8+
"fmt"
9+
"log/slog"
10+
"path/filepath"
11+
12+
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev"
13+
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/components"
14+
"github.com/microsoft/azure-linux-dev-tools/internal/providers/sourceproviders"
15+
"github.com/microsoft/azure-linux-dev-tools/internal/providers/sourceproviders/fedorasource"
16+
"github.com/microsoft/azure-linux-dev-tools/internal/utils/downloader"
17+
"github.com/microsoft/azure-linux-dev-tools/internal/utils/retry"
18+
"github.com/spf13/cobra"
19+
)
20+
21+
// DownloadSourcesOptions holds the options for the download-sources command.
22+
type DownloadSourcesOptions struct {
23+
Directory string
24+
OutputDir string
25+
LookasideBaseURIs []string
26+
ComponentName string
27+
LookasideDownloader fedorasource.FedoraSourceDownloader
28+
}
29+
30+
// OnAppInit registers the download-sources command as a subcommand of the given parent.
31+
func OnAppInit(_ *azldev.App, parentCmd *cobra.Command) {
32+
parentCmd.AddCommand(NewDownloadSourcesCmd())
33+
}
34+
35+
// NewDownloadSourcesCmd creates the download-sources cobra command.
36+
func NewDownloadSourcesCmd() *cobra.Command {
37+
var options DownloadSourcesOptions
38+
39+
cmd := &cobra.Command{
40+
Use: "download-sources",
41+
Short: "Download source files listed in a Fedora-format sources file",
42+
Long: `Download source files from a lookaside cache based on a Fedora-format
43+
'sources' file in the specified directory.
44+
45+
The command reads the 'sources' file, resolves the lookaside cache URI from
46+
the distro configuration, and downloads each listed file into the directory.
47+
Files that already exist in the directory are skipped.
48+
49+
Either --component or --lookaside-uri must be provided:
50+
51+
--component (-p) Uses the component's distro configuration to resolve
52+
the lookaside URI and package name.
53+
--lookaside-uri Provides the URI explicitly (no project config needed).
54+
Package name is derived from the directory name.`,
55+
Example: ` # Download sources for a component (uses component's distro config)
56+
azldev advanced download-sources -p curl
57+
58+
# Download sources using explicit lookaside URI (no config needed)
59+
azldev advanced download-sources \
60+
--lookaside-uri 'https://example.com/$pkg/$filename/$hashtype/$hash/$filename'
61+
62+
# Specify a different source directory
63+
azldev advanced download-sources -p curl -d ./path/to/sources/
64+
65+
# Download to a different output directory
66+
azldev advanced download-sources -p curl -o /tmp/output
67+
68+
# Try multiple lookaside URIs in order
69+
azldev advanced download-sources \
70+
--lookaside-uri 'https://cache1.example.com/$pkg/$filename/$hashtype/$hash/$filename' \
71+
--lookaside-uri 'https://cache2.example.com/$pkg/$filename/$hashtype/$hash/$filename'`,
72+
RunE: azldev.RunFuncWithoutRequiredConfig(func(env *azldev.Env) (interface{}, error) {
73+
if options.Directory == "" {
74+
options.Directory = "."
75+
}
76+
77+
return nil, DownloadSources(env, &options)
78+
}),
79+
}
80+
81+
cmd.Flags().StringVarP(&options.Directory, "dir", "d", "",
82+
"source directory containing the 'sources' file (defaults to current directory)")
83+
_ = cmd.MarkFlagDirname("dir")
84+
85+
cmd.Flags().StringVarP(&options.OutputDir, "output-dir", "o", "",
86+
"output directory for downloaded files (defaults to source directory)")
87+
_ = cmd.MarkFlagDirname("output-dir")
88+
89+
cmd.Flags().StringVarP(&options.ComponentName, "component", "p", "",
90+
"component name to resolve distro and package name from")
91+
92+
cmd.Flags().StringArrayVar(&options.LookasideBaseURIs, "lookaside-uri", nil,
93+
"explicit lookaside base URI(s) to try in order, first success wins "+
94+
"(can be specified multiple times)")
95+
96+
cmd.MarkFlagsOneRequired("component", "lookaside-uri")
97+
cmd.MarkFlagsMutuallyExclusive("component", "lookaside-uri")
98+
99+
return cmd
100+
}
101+
102+
// DownloadSources downloads source files from a lookaside cache into the specified directory.
103+
func DownloadSources(env *azldev.Env, options *DownloadSourcesOptions) error {
104+
packageName, lookasideBaseURIs, err := resolveDownloadParams(env, options)
105+
if err != nil {
106+
return err
107+
}
108+
109+
event := env.StartEvent("Downloading sources", "packageName", packageName)
110+
defer event.End()
111+
112+
lookasideDownloader := options.LookasideDownloader
113+
if lookasideDownloader == nil {
114+
lookasideDownloader, err = createLookasideDownloader(env)
115+
if err != nil {
116+
return err
117+
}
118+
}
119+
120+
// Build extract options.
121+
var extractOpts []fedorasource.ExtractOption
122+
if options.OutputDir != "" {
123+
extractOpts = append(extractOpts, fedorasource.WithOutputDir(options.OutputDir))
124+
}
125+
126+
// Try each lookaside base URI until one succeeds.
127+
var downloadErr error
128+
129+
for _, uri := range lookasideBaseURIs {
130+
slog.Info("Trying lookaside base URI", "uri", uri)
131+
132+
uriErr := lookasideDownloader.ExtractSourcesFromRepo(
133+
env, options.Directory, packageName, uri, nil, extractOpts...,
134+
)
135+
if uriErr == nil {
136+
downloadErr = nil
137+
138+
break
139+
}
140+
141+
slog.Warn("Failed to download sources from lookaside URI",
142+
"uri", uri, "error", uriErr)
143+
144+
downloadErr = errors.Join(downloadErr, uriErr)
145+
}
146+
147+
if downloadErr != nil {
148+
return fmt.Errorf("failed to download sources from any lookaside URI:\n%w",
149+
downloadErr)
150+
}
151+
152+
outputDir := options.Directory
153+
if options.OutputDir != "" {
154+
outputDir = options.OutputDir
155+
}
156+
157+
absOutputDir, absErr := filepath.Abs(outputDir)
158+
if absErr != nil {
159+
absOutputDir = outputDir
160+
}
161+
162+
slog.Info("Sources downloaded successfully", "outputDir", absOutputDir)
163+
164+
return nil
165+
}
166+
167+
// createLookasideDownloader builds the default [fedorasource.FedoraSourceDownloader]
168+
// from the environment's network and filesystem configuration.
169+
func createLookasideDownloader(env *azldev.Env) (fedorasource.FedoraSourceDownloader, error) {
170+
retryCfg := retry.DefaultConfig()
171+
if env.NetworkRetries() > 0 {
172+
retryCfg.MaxAttempts = env.NetworkRetries()
173+
}
174+
175+
httpDownloader, err := downloader.NewHTTPDownloader(env, env, env.FS())
176+
if err != nil {
177+
return nil, fmt.Errorf("failed to create HTTP downloader:\n%w", err)
178+
}
179+
180+
lookasideDownloader, err := fedorasource.NewFedoraRepoExtractorImpl(
181+
env, env.FS(), httpDownloader, retryCfg,
182+
)
183+
if err != nil {
184+
return nil, fmt.Errorf("failed to create lookaside downloader:\n%w", err)
185+
}
186+
187+
return lookasideDownloader, nil
188+
}
189+
190+
// resolveDownloadParams determines the package name and lookaside URIs.
191+
// In component mode, both are resolved from the component's config.
192+
// In standalone mode, lookaside URIs come from the flag and the package name
193+
// is derived from the directory basename.
194+
func resolveDownloadParams(
195+
env *azldev.Env, options *DownloadSourcesOptions,
196+
) (packageName string, lookasideBaseURIs []string, err error) {
197+
if len(options.LookasideBaseURIs) == 0 && options.ComponentName == "" {
198+
return "", nil, errors.New(
199+
"either --component or --lookaside-uri must be provided")
200+
}
201+
202+
// Standalone mode: --lookaside-uri provided.
203+
if len(options.LookasideBaseURIs) > 0 {
204+
packageName, err = resolvePackageNameFromDir(options)
205+
if err != nil {
206+
return "", nil, err
207+
}
208+
209+
return packageName, options.LookasideBaseURIs, nil
210+
}
211+
212+
// Component mode: --component provided.
213+
return resolveFromComponent(env, options)
214+
}
215+
216+
// resolvePackageNameFromDir derives the package name from the directory basename.
217+
func resolvePackageNameFromDir(options *DownloadSourcesOptions) (string, error) {
218+
absDir, err := filepath.Abs(options.Directory)
219+
if err != nil {
220+
return "", fmt.Errorf(
221+
"failed to resolve absolute path for %#q:\n%w",
222+
options.Directory, err)
223+
}
224+
225+
packageName := filepath.Base(absDir)
226+
227+
slog.Debug("Derived package name from directory name",
228+
"name", packageName, "dir", options.Directory)
229+
230+
return packageName, nil
231+
}
232+
233+
// resolveFromComponent resolves both the package name and lookaside URI
234+
// from a component's configuration.
235+
func resolveFromComponent(
236+
env *azldev.Env, options *DownloadSourcesOptions,
237+
) (packageName string, lookasideBaseURIs []string, err error) {
238+
resolver := components.NewResolver(env)
239+
240+
filter := &components.ComponentFilter{
241+
ComponentNamePatterns: []string{options.ComponentName},
242+
}
243+
244+
comps, err := resolver.FindComponents(filter)
245+
if err != nil {
246+
return "", nil, fmt.Errorf("failed to resolve component %#q:\n%w",
247+
options.ComponentName, err)
248+
}
249+
250+
if comps.Len() == 0 {
251+
return "", nil, fmt.Errorf("component %#q not found",
252+
options.ComponentName)
253+
}
254+
255+
if comps.Len() != 1 {
256+
return "", nil, fmt.Errorf(
257+
"expected exactly one component for %#q, got %d",
258+
options.ComponentName, comps.Len())
259+
}
260+
261+
component := comps.Components()[0]
262+
263+
// Derive package name from the component's upstream-name or component name.
264+
packageName = component.GetName()
265+
if upstreamName := component.GetConfig().Spec.UpstreamName; upstreamName != "" {
266+
packageName = upstreamName
267+
}
268+
269+
distro, err := sourceproviders.ResolveDistro(env, component)
270+
if err != nil {
271+
return "", nil, fmt.Errorf(
272+
"failed to resolve distro for component %#q:\n%w",
273+
options.ComponentName, err)
274+
}
275+
276+
if distro.Definition.LookasideBaseURI == "" {
277+
return "", nil, fmt.Errorf(
278+
"no lookaside base URI configured for distro %#q",
279+
distro.Ref.Name)
280+
}
281+
282+
return packageName, []string{distro.Definition.LookasideBaseURI}, nil
283+
}

0 commit comments

Comments
 (0)