Skip to content

Commit c14a7d9

Browse files
author
Antonio Salinas
committed
feat: Added download-source command
1 parent 0590d38 commit c14a7d9

14 files changed

+667
-63
lines changed

docs/user/reference/cli/azldev.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_download-sources.md

Lines changed: 71 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
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+
"strings"
12+
13+
"github.com/bmatcuk/doublestar/v4"
14+
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev"
15+
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/components"
16+
"github.com/microsoft/azure-linux-dev-tools/internal/providers/sourceproviders"
17+
"github.com/microsoft/azure-linux-dev-tools/internal/providers/sourceproviders/fedorasource"
18+
"github.com/microsoft/azure-linux-dev-tools/internal/utils/downloader"
19+
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileutils"
20+
"github.com/microsoft/azure-linux-dev-tools/internal/utils/retry"
21+
"github.com/spf13/cobra"
22+
)
23+
24+
// specFileExtension is the file extension for RPM spec files.
25+
const specFileExtension = ".spec"
26+
27+
// DownloadSourcesOptions holds the options for the download-sources command.
28+
type DownloadSourcesOptions struct {
29+
ComponentFilter components.ComponentFilter
30+
Directory string
31+
OutputDir string
32+
}
33+
34+
// OnAppInit registers the download-sources command as a top-level command.
35+
func OnAppInit(app *azldev.App) {
36+
app.AddTopLevelCommand(NewDownloadSourcesCmd())
37+
}
38+
39+
// NewDownloadSourcesCmd creates the download-sources cobra command.
40+
func NewDownloadSourcesCmd() *cobra.Command {
41+
var options DownloadSourcesOptions
42+
43+
cmd := &cobra.Command{
44+
Use: "download-sources [directory]",
45+
Short: "Download source files listed in a Fedora-format sources file",
46+
Long: `Download source files from a lookaside cache based on a Fedora-format
47+
'sources' file in the specified directory.
48+
49+
The command reads the 'sources' file, resolves the lookaside cache URI from
50+
the distro configuration, and downloads each listed file into the directory.
51+
Files that already exist in the directory are skipped.
52+
53+
The package name is derived from the .spec file in the directory. If a
54+
component is specified, its upstream-name override and distro configuration
55+
are used instead. When no component is given, the project's default distro
56+
is used.`,
57+
Example: ` # Download sources (package name derived from .spec file in directory)
58+
azldev download-sources ./path/to/sources/dir
59+
60+
# Download sources in the current directory
61+
azldev download-sources
62+
63+
# Download sources for a specific component
64+
azldev download-sources ./path/to/sources/dir -p curl
65+
66+
# Download sources to a different output directory
67+
azldev download-sources ./path/to/sources/dir -o /tmp/output`,
68+
Args: cobra.MaximumNArgs(1),
69+
RunE: azldev.RunFuncWithExtraArgs(func(env *azldev.Env, args []string) (interface{}, error) {
70+
options.Directory = "."
71+
if len(args) > 0 {
72+
options.Directory = args[0]
73+
}
74+
75+
return nil, DownloadSources(env, &options)
76+
}),
77+
ValidArgsFunction: func(
78+
_ *cobra.Command, _ []string, _ string,
79+
) ([]string, cobra.ShellCompDirective) {
80+
return nil, cobra.ShellCompDirectiveFilterDirs
81+
},
82+
}
83+
84+
components.AddComponentFilterOptionsToCommand(cmd, &options.ComponentFilter)
85+
86+
cmd.Flags().StringVarP(&options.OutputDir, "output-dir", "o", "",
87+
"output directory for downloaded files (defaults to source directory)")
88+
_ = cmd.MarkFlagDirname("output-dir")
89+
90+
azldev.ExportAsMCPTool(cmd)
91+
92+
return cmd
93+
}
94+
95+
// DownloadSources downloads source files from a lookaside cache into the specified directory.
96+
func DownloadSources(env *azldev.Env, options *DownloadSourcesOptions) error {
97+
packageName, lookasideBaseURI, err := resolveDownloadParams(env, options)
98+
if err != nil {
99+
return err
100+
}
101+
102+
slog.Info("Downloading sources from lookaside cache",
103+
"packageName", packageName,
104+
"outputDir", options.OutputDir,
105+
)
106+
107+
// Build retry config from environment.
108+
retryCfg := retry.DefaultConfig()
109+
if env.NetworkRetries() > 0 {
110+
retryCfg.MaxAttempts = env.NetworkRetries()
111+
}
112+
113+
// Create the HTTP downloader and lookaside source downloader.
114+
httpDownloader, err := downloader.NewHTTPDownloader(env, env, env.FS())
115+
if err != nil {
116+
return fmt.Errorf("failed to create HTTP downloader:\n%w", err)
117+
}
118+
119+
lookasideDownloader, err := fedorasource.NewFedoraRepoExtractorImpl(
120+
env, env.FS(), httpDownloader, retryCfg,
121+
)
122+
if err != nil {
123+
return fmt.Errorf("failed to create lookaside downloader:\n%w", err)
124+
}
125+
126+
// Determine where to download files.
127+
downloadDir := options.Directory
128+
if options.OutputDir != "" {
129+
downloadDir = options.OutputDir
130+
}
131+
132+
// If downloading to a different directory, copy the sources file there.
133+
if downloadDir != options.Directory {
134+
srcPath := filepath.Join(options.Directory, "sources")
135+
dstPath := filepath.Join(downloadDir, "sources")
136+
137+
if err := fileutils.MkdirAll(env.FS(), downloadDir); err != nil {
138+
return fmt.Errorf("failed to create output directory %#q:\n%w", downloadDir, err)
139+
}
140+
141+
if err := fileutils.CopyFile(env, env.FS(), srcPath, dstPath, fileutils.CopyFileOptions{}); err != nil {
142+
return fmt.Errorf("failed to copy sources file to output directory:\n%w", err)
143+
}
144+
}
145+
146+
// Download all sources listed in the sources file.
147+
err = lookasideDownloader.ExtractSourcesFromRepo(
148+
env, downloadDir, packageName, lookasideBaseURI, nil,
149+
)
150+
if err != nil {
151+
return fmt.Errorf("failed to download sources:\n%w", err)
152+
}
153+
154+
slog.Info("Sources downloaded successfully", "outputDir", options.OutputDir)
155+
156+
return nil
157+
}
158+
159+
// resolveDownloadParams determines the package name and lookaside URI, either from
160+
// a specified component or by reading the .spec file in the directory.
161+
func resolveDownloadParams(
162+
env *azldev.Env, options *DownloadSourcesOptions,
163+
) (packageName string, lookasideBaseURI string, err error) {
164+
if !options.ComponentFilter.HasNoCriteria() {
165+
return resolveFromComponent(env, options)
166+
}
167+
168+
return resolveFromSpecFile(env, options.Directory)
169+
}
170+
171+
// resolveFromComponent resolves the package name and lookaside URI from a component's config.
172+
func resolveFromComponent(
173+
env *azldev.Env, options *DownloadSourcesOptions,
174+
) (packageName string, lookasideBaseURI string, err error) {
175+
resolver := components.NewResolver(env)
176+
177+
comps, err := resolver.FindComponents(&options.ComponentFilter)
178+
if err != nil {
179+
return "", "", fmt.Errorf("failed to resolve components:\n%w", err)
180+
}
181+
182+
if comps.Len() == 0 {
183+
return "", "", errors.New("no components were selected; " +
184+
"please use command-line options to indicate which component to use")
185+
}
186+
187+
if comps.Len() != 1 {
188+
return "", "", fmt.Errorf("expected exactly one component, got %d", comps.Len())
189+
}
190+
191+
component := comps.Components()[0]
192+
193+
packageName = component.GetName()
194+
if upstreamName := component.GetConfig().Spec.UpstreamName; upstreamName != "" {
195+
packageName = upstreamName
196+
}
197+
198+
distro, err := sourceproviders.ResolveDistro(env, component)
199+
if err != nil {
200+
return "", "", fmt.Errorf("failed to resolve distro for component %#q:\n%w", component.GetName(), err)
201+
}
202+
203+
lookasideBaseURI = distro.Definition.LookasideBaseURI
204+
if lookasideBaseURI == "" {
205+
return "", "", fmt.Errorf("no lookaside base URI configured for distro %#q", distro.Ref.Name)
206+
}
207+
208+
return packageName, lookasideBaseURI, nil
209+
}
210+
211+
// resolveFromSpecFile derives the package name from a .spec file in the directory
212+
// and uses the project's default distro for the lookaside URI.
213+
func resolveFromSpecFile(
214+
env *azldev.Env, directory string,
215+
) (packageName string, lookasideBaseURI string, err error) {
216+
dirExists, err := fileutils.DirExists(env.FS(), directory)
217+
if err != nil {
218+
return "", "", fmt.Errorf("failed to check directory %#q:\n%w", directory, err)
219+
}
220+
221+
if !dirExists {
222+
return "", "", fmt.Errorf("directory %#q does not exist", directory)
223+
}
224+
225+
specPattern := filepath.Join(directory, "*"+specFileExtension)
226+
227+
specFiles, err := fileutils.Glob(env.FS(), specPattern, doublestar.WithFilesOnly())
228+
if err != nil {
229+
return "", "", fmt.Errorf("failed to search for spec files in %#q:\n%w", directory, err)
230+
}
231+
232+
if len(specFiles) == 0 {
233+
return "", "", fmt.Errorf("no .spec file found in %#q; "+
234+
"specify a component with -p to provide the package name", directory)
235+
}
236+
237+
if len(specFiles) > 1 {
238+
return "", "", fmt.Errorf("multiple .spec files found in %#q; "+
239+
"specify a component with -p to select one", directory)
240+
}
241+
242+
packageName = strings.TrimSuffix(filepath.Base(specFiles[0]), specFileExtension)
243+
244+
slog.Debug("Derived package name from spec filename", "name", packageName, "specFile", specFiles[0])
245+
246+
lookasideBaseURI, err = resolveLookasideURI(env)
247+
if err != nil {
248+
return "", "", err
249+
}
250+
251+
return packageName, lookasideBaseURI, nil
252+
}
253+
254+
// resolveLookasideURI finds the lookaside base URI by checking the default distro first,
255+
// then following the upstream distro reference if needed.
256+
func resolveLookasideURI(env *azldev.Env) (string, error) {
257+
distroDef, distroVersionDef, err := env.Distro()
258+
if err != nil {
259+
return "", fmt.Errorf("failed to resolve default distro:\n%w", err)
260+
}
261+
262+
// If the default distro itself has a lookaside URI, use it directly.
263+
if distroDef.LookasideBaseURI != "" {
264+
return distroDef.LookasideBaseURI, nil
265+
}
266+
267+
// Otherwise, follow the upstream distro reference from the default component config.
268+
upstreamRef := distroVersionDef.DefaultComponentConfig.Spec.UpstreamDistro
269+
if upstreamRef.Name == "" {
270+
return "", errors.New("no lookaside base URI configured for the default distro, " +
271+
"and no upstream distro reference found; specify a component with -p")
272+
}
273+
274+
upstreamDef, _, err := env.ResolveDistroRef(upstreamRef)
275+
if err != nil {
276+
return "", fmt.Errorf("failed to resolve upstream distro %#q:\n%w", upstreamRef.Name, err)
277+
}
278+
279+
if upstreamDef.LookasideBaseURI == "" {
280+
return "", fmt.Errorf("no lookaside base URI configured for upstream distro %#q", upstreamRef.Name)
281+
}
282+
283+
return upstreamDef.LookasideBaseURI, nil
284+
}

0 commit comments

Comments
 (0)