Skip to content

Commit da3c04e

Browse files
authored
Merge pull request #23 from open-edge-platform/refactor-main-package
Refactored the Refactor main package into modular command structure
2 parents e6582bf + dcb0b55 commit da3c04e

7 files changed

Lines changed: 528 additions & 465 deletions

File tree

cmd/image-composer/build.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"github.com/open-edge-platform/image-composer/internal/config"
9+
"github.com/open-edge-platform/image-composer/internal/pkgfetcher"
10+
"github.com/open-edge-platform/image-composer/internal/provider"
11+
_ "github.com/open-edge-platform/image-composer/internal/provider/azurelinux3" // register provider
12+
_ "github.com/open-edge-platform/image-composer/internal/provider/elxr12" // register provider
13+
_ "github.com/open-edge-platform/image-composer/internal/provider/emt3_0" // register provider
14+
"github.com/open-edge-platform/image-composer/internal/rpmutils"
15+
utils "github.com/open-edge-platform/image-composer/internal/utils/logger"
16+
"github.com/spf13/cobra"
17+
)
18+
19+
// Build command flags
20+
var (
21+
workers int = -1 // -1 means use config file value
22+
cacheDir string = "" // Empty means use config file value
23+
workDir string = "" // Empty means use config file value
24+
verbose bool = false
25+
dotFile string = ""
26+
)
27+
28+
// createBuildCommand creates the build subcommand
29+
func createBuildCommand() *cobra.Command {
30+
buildCmd := &cobra.Command{
31+
Use: "build [flags] SPEC_FILE",
32+
Short: "Build a Linux distribution image",
33+
Long: `Build a Linux distribution image based on the specified spec file.
34+
The spec file should be in JSON format according to the schema.`,
35+
Args: cobra.ExactArgs(1),
36+
RunE: executeBuild,
37+
ValidArgsFunction: jsonFileCompletion,
38+
}
39+
40+
// Add flags
41+
buildCmd.Flags().IntVarP(&workers, "workers", "w", -1,
42+
"Number of concurrent download workers")
43+
buildCmd.Flags().StringVarP(&cacheDir, "cache-dir", "d", "",
44+
"Package cache directory")
45+
buildCmd.Flags().StringVar(&workDir, "work-dir", "",
46+
"Working directory for builds")
47+
buildCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output")
48+
buildCmd.Flags().StringVarP(&dotFile, "dotfile", "f", "", "Generate a dot file for the dependency graph")
49+
50+
return buildCmd
51+
}
52+
53+
// executeBuild handles the build command execution logic
54+
func executeBuild(cmd *cobra.Command, args []string) error {
55+
// Parse command-line flags and override global config
56+
if cmd.Flags().Changed("workers") {
57+
globalConfig.Workers = workers
58+
}
59+
if cmd.Flags().Changed("cache-dir") {
60+
globalConfig.CacheDir = cacheDir
61+
}
62+
if cmd.Flags().Changed("work-dir") {
63+
globalConfig.WorkDir = workDir
64+
}
65+
66+
logger := utils.Logger()
67+
68+
// Check if spec file is provided as first positional argument
69+
if len(args) < 1 {
70+
return fmt.Errorf("no spec file provided, usage: image-composer build [flags] SPEC_FILE")
71+
}
72+
specFile := args[0]
73+
74+
// Load and validate the configuration
75+
bc, err := config.Load(specFile)
76+
if err != nil {
77+
return fmt.Errorf("loading spec file: %v", err)
78+
}
79+
80+
providerName := bc.Distro + bc.Version
81+
82+
// Get provider by name
83+
p, ok := provider.Get(providerName)
84+
if !ok {
85+
return fmt.Errorf("provider not found: %s", providerName)
86+
}
87+
88+
// Initialize provider
89+
if err := p.Init(bc); err != nil {
90+
return fmt.Errorf("provider init: %v", err)
91+
}
92+
93+
// Fetch the entire package list
94+
all, err := p.Packages()
95+
if err != nil {
96+
return fmt.Errorf("getting packages: %v", err)
97+
}
98+
99+
// Match the packages in the build spec against all the packages
100+
req, err := p.MatchRequested(bc.Packages, all)
101+
if err != nil {
102+
return fmt.Errorf("matching packages: %v", err)
103+
}
104+
logger.Infof("matched a total of %d packages", len(req))
105+
if verbose {
106+
for _, pkg := range req {
107+
logger.Infof("-> %s", pkg.Name)
108+
}
109+
}
110+
111+
// Resolve the dependencies of the requested packages
112+
needed, err := p.Resolve(req, all)
113+
if err != nil {
114+
return fmt.Errorf("resolving packages: %v", err)
115+
}
116+
logger.Infof("resolved %d packages", len(needed))
117+
118+
// If a dot file is specified, generate the dependency graph
119+
if dotFile != "" {
120+
if err := rpmutils.GenerateDot(needed, dotFile); err != nil {
121+
logger.Errorf("generating dot file: %v", err)
122+
}
123+
}
124+
125+
// Extract URLs
126+
urls := make([]string, len(needed))
127+
for i, pkg := range needed {
128+
urls[i] = pkg.URL
129+
}
130+
131+
// Ensure cache directory exists
132+
absCacheDir, err := filepath.Abs(globalConfig.CacheDir)
133+
if err != nil {
134+
return fmt.Errorf("resolving cache directory: %v", err)
135+
}
136+
if err := os.MkdirAll(absCacheDir, 0755); err != nil {
137+
return fmt.Errorf("creating cache directory %s: %v", absCacheDir, err)
138+
}
139+
140+
// Ensure work directory exists
141+
absWorkDir, err := filepath.Abs(globalConfig.WorkDir)
142+
if err != nil {
143+
return fmt.Errorf("resolving work directory: %v", err)
144+
}
145+
if err := os.MkdirAll(absWorkDir, 0755); err != nil {
146+
return fmt.Errorf("creating work directory %s: %v", absWorkDir, err)
147+
}
148+
149+
// Download packages using configured workers and cache directory
150+
logger.Infof("downloading %d packages to %s using %d workers", len(urls), absCacheDir, globalConfig.Workers)
151+
if err := pkgfetcher.FetchPackages(urls, absCacheDir, globalConfig.Workers); err != nil {
152+
return fmt.Errorf("fetch failed: %v", err)
153+
}
154+
logger.Info("all downloads complete")
155+
156+
// Verify downloaded packages
157+
if err := p.Validate(globalConfig.CacheDir); err != nil {
158+
return fmt.Errorf("verification failed: %v", err)
159+
}
160+
161+
logger.Info("build completed successfully")
162+
return nil
163+
}
164+
165+
// jsonFileCompletion helps with suggesting JSON files for spec file argument
166+
func jsonFileCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
167+
return []string{"*.json"}, cobra.ShellCompDirectiveFilterFileExt
168+
}

cmd/image-composer/completion.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
10+
"github.com/spf13/cobra"
11+
)
12+
13+
// createInstallCompletionCommand creates the install-completion subcommand
14+
func createInstallCompletionCommand() *cobra.Command {
15+
installCompletionCmd := &cobra.Command{
16+
Use: "install-completion",
17+
Short: "Install shell completion script",
18+
Long: `Install shell completion script for Bash, Zsh, Fish, or PowerShell.
19+
Automatically detects your shell and installs the appropriate completion script.`,
20+
RunE: executeInstallCompletion,
21+
}
22+
23+
// Add flags
24+
installCompletionCmd.Flags().String("shell", "", "Specify shell type (bash, zsh, fish, powershell)")
25+
installCompletionCmd.Flags().Bool("force", false, "Force overwrite existing completion files")
26+
27+
return installCompletionCmd
28+
}
29+
30+
// executeInstallCompletion handles installation of shell completion scripts
31+
func executeInstallCompletion(cmd *cobra.Command, args []string) error {
32+
shellType := ""
33+
userForce := false
34+
35+
// Process flags
36+
if cmd.Flags().Changed("shell") {
37+
var err error
38+
shellType, err = cmd.Flags().GetString("shell")
39+
if err != nil {
40+
return err
41+
}
42+
}
43+
44+
if cmd.Flags().Changed("force") {
45+
var err error
46+
userForce, err = cmd.Flags().GetBool("force")
47+
if err != nil {
48+
return err
49+
}
50+
}
51+
52+
// If no shell specified, detect current shell
53+
if shellType == "" {
54+
shellEnv := os.Getenv("SHELL")
55+
if shellEnv != "" {
56+
switch {
57+
case strings.Contains(shellEnv, "bash"):
58+
shellType = "bash"
59+
case strings.Contains(shellEnv, "zsh"):
60+
shellType = "zsh"
61+
case strings.Contains(shellEnv, "fish"):
62+
shellType = "fish"
63+
default:
64+
return fmt.Errorf("unsupported shell: %s. Please specify shell with --shell flag", shellEnv)
65+
}
66+
} else {
67+
// On Windows, we may not have $SHELL
68+
if os.Getenv("PSModulePath") != "" {
69+
shellType = "powershell"
70+
} else {
71+
return fmt.Errorf("could not detect shell. Please specify with --shell flag")
72+
}
73+
}
74+
}
75+
76+
// Generate completion script
77+
var buf bytes.Buffer
78+
switch shellType {
79+
case "bash":
80+
if err := cmd.Root().GenBashCompletion(&buf); err != nil {
81+
return fmt.Errorf("error generating Bash completion: %w", err)
82+
}
83+
case "zsh":
84+
if err := cmd.Root().GenZshCompletion(&buf); err != nil {
85+
return fmt.Errorf("error generating Zsh completion: %w", err)
86+
}
87+
case "fish":
88+
if err := cmd.Root().GenFishCompletion(&buf, true); err != nil {
89+
return fmt.Errorf("error generating Fish completion: %w", err)
90+
}
91+
case "powershell":
92+
if err := cmd.Root().GenPowerShellCompletion(&buf); err != nil {
93+
return fmt.Errorf("error generating PowerShell completion: %w", err)
94+
}
95+
default:
96+
return fmt.Errorf("unsupported shell type: %s", shellType)
97+
}
98+
99+
// Determine where to save the completion script
100+
var targetPath string
101+
homeDir, err := os.UserHomeDir()
102+
if err != nil {
103+
return fmt.Errorf("could not determine home directory: %v", err)
104+
}
105+
106+
switch shellType {
107+
case "bash":
108+
// Try to detect if bash-completion is installed
109+
completionDir := "/etc/bash_completion.d"
110+
if _, err := os.Stat(completionDir); os.IsNotExist(err) {
111+
// Fallback to user's directory
112+
completionDir = filepath.Join(homeDir, ".bash_completion.d")
113+
if _, err := os.Stat(completionDir); os.IsNotExist(err) {
114+
if err := os.MkdirAll(completionDir, 0755); err != nil {
115+
return fmt.Errorf("could not create directory %s: %v", completionDir, err)
116+
}
117+
}
118+
}
119+
targetPath = filepath.Join(completionDir, "image-composer.bash")
120+
case "zsh":
121+
completionDir := filepath.Join(homeDir, ".zsh/completion")
122+
if _, err := os.Stat(completionDir); os.IsNotExist(err) {
123+
if err := os.MkdirAll(completionDir, 0755); err != nil {
124+
return fmt.Errorf("could not create directory %s: %v", completionDir, err)
125+
}
126+
}
127+
targetPath = filepath.Join(completionDir, "_image-composer")
128+
case "fish":
129+
completionDir := filepath.Join(homeDir, ".config/fish/completions")
130+
if _, err := os.Stat(completionDir); os.IsNotExist(err) {
131+
if err := os.MkdirAll(completionDir, 0755); err != nil {
132+
return fmt.Errorf("could not create directory %s: %v", completionDir, err)
133+
}
134+
}
135+
targetPath = filepath.Join(completionDir, "image-composer.fish")
136+
case "powershell":
137+
profilePath := filepath.Join(homeDir, "Documents/WindowsPowerShell")
138+
if _, err := os.Stat(profilePath); os.IsNotExist(err) {
139+
if err := os.MkdirAll(profilePath, 0755); err != nil {
140+
return fmt.Errorf("could not create directory %s: %v", profilePath, err)
141+
}
142+
}
143+
targetPath = filepath.Join(profilePath, "image-composer-completion.ps1")
144+
}
145+
146+
// Check if file exists
147+
if _, err := os.Stat(targetPath); err == nil && !userForce {
148+
return fmt.Errorf("completion file already exists at %s. Use --force to overwrite", targetPath)
149+
}
150+
151+
// Write completion script to file
152+
if err := os.WriteFile(targetPath, buf.Bytes(), 0644); err != nil {
153+
return fmt.Errorf("could not write completion file: %v", err)
154+
}
155+
156+
fmt.Printf("Shell completion installed for %s at %s\n", shellType, targetPath)
157+
fmt.Printf("Restart your shell or source your profile to enable completion.\n")
158+
159+
return nil
160+
}

0 commit comments

Comments
 (0)