Skip to content

Commit 90fef41

Browse files
committed
refactor: add service pattern
1 parent 8fb48d9 commit 90fef41

File tree

8 files changed

+317
-230
lines changed

8 files changed

+317
-230
lines changed

cmd/build.go

Lines changed: 19 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -1,200 +1,35 @@
11
package cmd
22

33
import (
4-
"fmt"
5-
"os"
6-
"os/exec"
7-
"path/filepath"
8-
"strconv"
9-
"strings"
10-
4+
"github.com/Norgate-AV/spc/internal/build"
115
"github.com/spf13/cobra"
12-
"github.com/spf13/viper"
13-
14-
"github.com/Norgate-AV/spc/internal/compiler"
15-
"github.com/Norgate-AV/spc/internal/config"
166
)
177

188
var buildCmd = &cobra.Command{
19-
Use: "build",
20-
Short: "Build SIMPL+ file(s)",
21-
Long: `Build a SIMPL+ file(s) for the specified target series.`,
22-
RunE: func(cmd *cobra.Command, args []string) error {
23-
return runBuild(cmd, args)
24-
},
9+
Use: "build",
10+
Short: "Build SIMPL+ file(s)",
11+
Long: `Build a SIMPL+ file(s) for the specified target series.`,
12+
RunE: runBuild,
2513
SilenceUsage: true,
2614
}
2715

2816
func runBuild(cmd *cobra.Command, args []string) error {
29-
if len(args) == 0 {
30-
return fmt.Errorf("no files specified")
31-
}
32-
33-
cfg, err := loadBuildConfig(cmd, args)
34-
if err != nil {
35-
return err
36-
}
37-
38-
cmdArgs, err := buildCommandArgs(cfg, args)
39-
if err != nil {
40-
return err
41-
}
42-
43-
series := parseTarget(cfg.Target)
44-
45-
if cfg.Verbose {
46-
fmt.Printf("Compiler: %s\nTarget: %s\nSeries: %v\nFiles: %v\nOut: %s\nUsersPlusFolders: %v\nCommand: %s %s\n", cfg.CompilerPath, cfg.Target, series, args, cfg.OutputFile, cfg.UserFolders, cfg.CompilerPath, strings.Join(cmdArgs, " "))
47-
}
17+
service := build.NewBuildService()
18+
return service.Build(cmd, args)
4819

49-
c := execCommand(cfg.CompilerPath, cmdArgs...)
50-
if cmd, ok := c.(*exec.Cmd); ok {
51-
cmd.Stdout = os.Stdout
52-
cmd.Stderr = os.Stderr
53-
}
54-
55-
err = c.Run()
56-
if err != nil {
57-
if exitErr, ok := err.(*exec.ExitError); ok {
58-
code := exitErr.ExitCode()
59-
if compiler.IsSuccess(code) {
60-
// Crestron compiler success (may have warnings)
61-
return nil
62-
}
63-
64-
// Print descriptive error message
65-
fmt.Fprintf(os.Stderr, "Compilation failed (exit code %d): %s\n", code, codes.GetErrorMessage(code))
66-
}
67-
68-
return err
69-
}
70-
71-
return nil
72-
}
73-
74-
func buildCommandArgs(cfg *config.Config, files []string) ([]string, error) {
75-
series := parseTarget(cfg.Target)
76-
if len(series) == 0 {
77-
return nil, fmt.Errorf("invalid target series")
78-
}
79-
80-
var cmdArgs []string
81-
cmdArgs = append(cmdArgs, "/target")
82-
cmdArgs = append(cmdArgs, series...)
83-
84-
for _, folder := range cfg.UserFolders {
85-
if folder != "" {
86-
cmdArgs = append(cmdArgs, "/usersplusfolder", folder)
87-
}
88-
}
89-
90-
cmdArgs = append(cmdArgs, "/rebuild")
91-
92-
for _, file := range files {
93-
absFile, err := filepath.Abs(file)
94-
if err != nil {
95-
return nil, fmt.Errorf("failed to resolve absolute path for %s: %w", file, err)
96-
}
97-
98-
cmdArgs = append(cmdArgs, absFile)
99-
}
100-
101-
if cfg.OutputFile != "" {
102-
cmdArgs = append(cmdArgs, "/out", cfg.OutputFile)
103-
}
104-
105-
if cfg.Silent {
106-
cmdArgs = append(cmdArgs, "/silent")
107-
}
108-
109-
return cmdArgs, nil
110-
}
20+
// Resolve config
21+
// cfg, err := config.Load()
22+
// if err != nil {
23+
// return fmt.Errorf("error loading config: %w", err)
24+
// }
11125

112-
func parseTarget(t string) []string {
113-
series := make([]string, 0)
26+
// fmt.Printf("%+v\n", cfg)
27+
// fmt.Printf("%+v\n", args)
11428

115-
for _, r := range t {
116-
if s := int(r - '0'); s >= 2 && s <= 4 {
117-
series = append(series, "series"+strconv.Itoa(s))
118-
}
119-
}
120-
121-
return series
122-
}
123-
124-
func findLocalConfig(dir string) string {
125-
for {
126-
for _, ext := range []string{"yml", "yaml", "json", "toml"} {
127-
path := filepath.Join(dir, ".spc."+ext)
128-
129-
if _, err := os.Stat(path); err == nil {
130-
return path
131-
}
132-
}
133-
134-
parent := filepath.Dir(dir)
135-
if parent == dir {
136-
break
137-
}
138-
139-
dir = parent
140-
}
141-
142-
return ""
143-
}
144-
145-
func loadBuildConfig(cmd *cobra.Command, args []string) (*config.Config, error) {
146-
// Set defaults
147-
viper.SetDefault("compiler_path", "C:/Program Files (x86)/Crestron/Simpl/SPlusCC.exe")
148-
viper.SetDefault("target", "234")
149-
viper.SetDefault("silent", false)
150-
viper.SetDefault("verbose", false)
151-
152-
// global config
153-
appdata := os.Getenv("APPDATA")
154-
if appdata != "" {
155-
globalDir := filepath.Join(appdata, "spc")
156-
157-
for _, ext := range []string{"yml", "yaml", "json", "toml"} {
158-
globalPath := filepath.Join(globalDir, "config."+ext)
159-
160-
if _, err := os.Stat(globalPath); err == nil {
161-
viper.SetConfigFile(globalPath)
162-
163-
if err := viper.ReadInConfig(); err == nil {
164-
break
165-
}
166-
}
167-
}
168-
}
169-
170-
// local config
171-
if len(args) > 0 {
172-
absFirstFile, err := filepath.Abs(args[0])
173-
if err != nil {
174-
return nil, fmt.Errorf("failed to resolve absolute path for first file: %w", err)
175-
}
176-
177-
dir := filepath.Dir(absFirstFile)
178-
localPath := findLocalConfig(dir)
179-
if localPath != "" {
180-
viper.SetConfigFile(localPath)
181-
_ = viper.ReadInConfig()
182-
}
183-
}
184-
185-
// bind flags
186-
_ = viper.BindPFlag("target", cmd.Flags().Lookup("target"))
187-
_ = viper.BindPFlag("verbose", cmd.Flags().Lookup("verbose"))
188-
_ = viper.BindPFlag("out", cmd.Flags().Lookup("out"))
189-
_ = viper.BindPFlag("usersplusfolder", cmd.Flags().Lookup("usersplusfolder"))
190-
191-
return config.Load()
192-
}
193-
194-
var execCommand = func(name string, args ...string) Commander {
195-
return exec.Command(name, args...)
196-
}
29+
// Validate input
30+
// if len(args) == 0 {
31+
// return cmd.Help()
32+
// }
19733

198-
type Commander interface {
199-
Run() error
34+
// return nil
20035
}

cmd/root.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/Norgate-AV/spc/internal/version"
88
"github.com/spf13/cobra"
9+
"github.com/spf13/viper"
910
)
1011

1112
var rootCmd = &cobra.Command{
@@ -32,4 +33,9 @@ func init() {
3233
rootCmd.PersistentFlags().StringP("out", "o", "", "Output file for compilation logs")
3334
rootCmd.PersistentFlags().StringSliceP("usersplusfolder", "u", []string{}, "User SIMPL+ folders")
3435
rootCmd.AddCommand(buildCmd)
36+
37+
viper.SetDefault("compiler_path", "C:/Program Files (x86)/Crestron/Simpl/SPlusCC.exe")
38+
viper.SetDefault("target", "234")
39+
viper.SetDefault("silent", false)
40+
viper.SetDefault("verbose", false)
3541
}

internal/compiler/builder.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package compiler
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
"strings"
9+
10+
"github.com/Norgate-AV/spc/internal/config"
11+
"github.com/Norgate-AV/spc/internal/utils"
12+
)
13+
14+
// Commander interface for testing
15+
type Commander interface {
16+
Run() error
17+
}
18+
19+
// CommandBuilder handles building compiler commands
20+
type CommandBuilder struct {
21+
execCommand func(name string, args ...string) Commander
22+
}
23+
24+
// NewCommandBuilder creates a new command builder
25+
func NewCommandBuilder() *CommandBuilder {
26+
return &CommandBuilder{
27+
execCommand: func(name string, args ...string) Commander {
28+
return exec.Command(name, args...)
29+
},
30+
}
31+
}
32+
33+
// BuildCommandArgs builds the command arguments for the compiler
34+
func (cb *CommandBuilder) BuildCommandArgs(cfg *config.Config, files []string) ([]string, error) {
35+
series := utils.ParseTarget(cfg.Target)
36+
if len(series) == 0 {
37+
return nil, fmt.Errorf("invalid target series")
38+
}
39+
40+
var cmdArgs []string
41+
cmdArgs = append(cmdArgs, "/target")
42+
cmdArgs = append(cmdArgs, series...)
43+
44+
for _, folder := range cfg.UserFolders {
45+
if folder != "" {
46+
cmdArgs = append(cmdArgs, "/usersplusfolder", folder)
47+
}
48+
}
49+
50+
cmdArgs = append(cmdArgs, "/rebuild")
51+
52+
for _, file := range files {
53+
absFile, err := filepath.Abs(file)
54+
if err != nil {
55+
return nil, fmt.Errorf("failed to resolve absolute path for %s: %w", file, err)
56+
}
57+
58+
cmdArgs = append(cmdArgs, absFile)
59+
}
60+
61+
if cfg.OutputFile != "" {
62+
cmdArgs = append(cmdArgs, "/out", cfg.OutputFile)
63+
}
64+
65+
if cfg.Silent {
66+
cmdArgs = append(cmdArgs, "/silent")
67+
}
68+
69+
return cmdArgs, nil
70+
}
71+
72+
// ExecuteCommand executes the compiler command
73+
func (cb *CommandBuilder) ExecuteCommand(compilerPath string, cmdArgs []string) error {
74+
c := cb.execCommand(compilerPath, cmdArgs...)
75+
if cmd, ok := c.(*exec.Cmd); ok {
76+
cmd.Stdout = os.Stdout
77+
cmd.Stderr = os.Stderr
78+
}
79+
80+
err := c.Run()
81+
if err != nil {
82+
if exitErr, ok := err.(*exec.ExitError); ok {
83+
code := exitErr.ExitCode()
84+
if IsSuccess(code) {
85+
// Crestron compiler success (may have warnings)
86+
return nil
87+
}
88+
89+
// Print descriptive error message
90+
fmt.Fprintf(os.Stderr, "Compilation failed (exit code %d): %s\n", code, GetErrorMessage(code))
91+
}
92+
93+
return err
94+
}
95+
96+
return nil
97+
}
98+
99+
// PrintBuildInfo prints verbose build information
100+
func (cb *CommandBuilder) PrintBuildInfo(cfg *config.Config, series []string, args []string, cmdArgs []string) {
101+
fmt.Printf("Compiler: %s\nTarget: %s\nSeries: %v\nFiles: %v\nOut: %s\nUsersPlusFolders: %v\nCommand: %s %s\n",
102+
cfg.CompilerPath, cfg.Target, series, args, cfg.OutputFile, cfg.UserFolders, cfg.CompilerPath, strings.Join(cmdArgs, " "))
103+
}

internal/compiler/compiler.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package compiler
2+
3+
import (
4+
"fmt"
5+
"path/filepath"
6+
7+
"github.com/Norgate-AV/spc/internal/config"
8+
"github.com/Norgate-AV/spc/internal/utils"
9+
)
10+
11+
type ShellCommand struct {
12+
Path string
13+
Args []string
14+
}
15+
16+
func GetBuildCommand(cfg *config.Config, files []string) (*ShellCommand, error) {
17+
series := utils.ParseTarget(cfg.Target)
18+
if len(series) == 0 {
19+
return nil, fmt.Errorf("invalid target series")
20+
}
21+
22+
var cmdArgs []string
23+
cmdArgs = append(cmdArgs, "/target")
24+
cmdArgs = append(cmdArgs, series...)
25+
26+
for _, folder := range cfg.UserFolders {
27+
if folder != "" {
28+
cmdArgs = append(cmdArgs, "/usersplusfolder", folder)
29+
}
30+
}
31+
32+
cmdArgs = append(cmdArgs, "/rebuild")
33+
34+
for _, file := range files {
35+
absFile, err := filepath.Abs(file)
36+
if err != nil {
37+
return nil, fmt.Errorf("failed to resolve absolute path for %s: %w", file, err)
38+
}
39+
40+
cmdArgs = append(cmdArgs, absFile)
41+
}
42+
43+
if cfg.OutputFile != "" {
44+
cmdArgs = append(cmdArgs, "/out", cfg.OutputFile)
45+
}
46+
47+
if cfg.Silent {
48+
cmdArgs = append(cmdArgs, "/silent")
49+
}
50+
51+
return &ShellCommand{
52+
Path: cfg.CompilerPath,
53+
Args: cmdArgs,
54+
}, nil
55+
}

0 commit comments

Comments
 (0)