@@ -22,7 +22,7 @@ var execCommand = func(cmd *exec.Cmd) error {
2222}
2323
2424// mergeOrReplaceEnvVars merges new environment variables into an existing slice,
25- // replacing any existing variables with the same key
25+ // replacing any existing variables with the same key.
2626func mergeOrReplaceEnvVars (existing []string , newVars []string ) []string {
2727 result := make ([]string , len (existing ))
2828 copy (result , existing )
@@ -53,15 +53,40 @@ func mergeOrReplaceEnvVars(existing []string, newVars []string) []string {
5353 return result
5454}
5555
56- func determineModuleDirectory (goPrivate , fullModulePath string ) (string , error ) {
56+ // determineModuleDirectory locates the directory to build from.
57+ // - Local path (absolute or "./relative"): resolve and return the directory (no download).
58+ func determineModuleDirectoryLocal (pluginKey , moduleURI string ) (string , error ) {
59+ log .Printf ("%s - resolving local module path %q" , pluginKey , moduleURI )
60+ abs , err := filepath .Abs (moduleURI )
61+ if err != nil {
62+ return "" , fmt .Errorf ("failed to resolve local module path %q: %w" , moduleURI , err )
63+ }
64+ info , err := os .Stat (abs )
65+ if err != nil {
66+ return "" , fmt .Errorf ("local module path %q not accessible: %w" , abs , err )
67+ }
68+ if ! info .IsDir () {
69+ return "" , fmt .Errorf ("local module path %q is not a directory" , abs )
70+ }
71+ return abs , nil
72+ }
73+
74+ // determineModuleDirectory locates the directory to build from.
75+ // - Remote module path (e.g., "github.com/org/repo@ref"): use `go mod download -json` to get a module cache dir.
76+ func determineModuleDirectoryRemote (pluginKey , moduleURI , gitRef , goPrivate string ) (string , error ) {
77+ fullModulePath := moduleURI
78+ if gitRef != "" {
79+ fullModulePath = fmt .Sprintf ("%s@%s" , moduleURI , gitRef )
80+ }
81+ log .Printf ("%s - downloading remote module %s" , pluginKey , fullModulePath )
82+
5783 cmd := exec .Command ("go" , "mod" , "download" , "-json" , fullModulePath )
5884 var out bytes.Buffer
5985 cmd .Stdout = & out
6086 cmd .Stderr = os .Stderr
6187
6288 if goPrivate != "" {
6389 // Inherit the current environment and override GOPRIVATE.
64- // Note: Not really sure why this is needed - tried to simplify existing logic
6590 cmd .Env = append (os .Environ (), "GOPRIVATE=" + goPrivate )
6691 }
6792
@@ -81,13 +106,18 @@ func determineModuleDirectory(goPrivate, fullModulePath string) (string, error)
81106 return result .Dir , nil
82107}
83108
84- func determineGoFlags (defaultGoFlags , pluginGoFlags string ) ([]string , error ) {
109+ // determineGoFlags resolves go build flags in priority order:
110+ // 1) CL_PLUGIN_GOFLAGS env var (overrides config)
111+ // 2) defaults.GoFlags
112+ // 3) plugin-specific goflags appended
113+ // It validates flags if any are present.
114+ func determineGoFlags (pluginKey , defaultGoFlags , pluginGoFlags string ) ([]string , error ) {
85115 var flags []string
86116 parser := shellwords .NewParser ()
87117
88118 // Determine base flags
89119 if envGoFlags := os .Getenv ("CL_PLUGIN_GOFLAGS" ); envGoFlags != "" {
90- log .Printf ("Overriding config's default goflags with CL_PLUGIN_GOFLAGS env var: %s" , envGoFlags )
120+ log .Printf ("%s - overriding config's default goflags with CL_PLUGIN_GOFLAGS env var: %s" , pluginKey , envGoFlags )
91121 f , err := parser .Parse (envGoFlags )
92122 if err != nil {
93123 return nil , err
@@ -120,155 +150,170 @@ func determineGoFlags(defaultGoFlags, pluginGoFlags string) ([]string, error) {
120150 return flags , nil
121151}
122152
123- func determineInstallArg (installPath , moduleURI string ) string {
124- // Determine the actual argument for 'go install' based on installPath and moduleURI.
125- // - installPath is the user-provided path from YAML (no environment variable expansion).
126- // - moduleURI is the URI of the module being downloaded and installed (no environment variable expansion).
127- // The 'go install' command will be run with cmd.Dir set to the root of the downloaded moduleURI.
128- // Therefore, installArg must be "." or a path starting with "./" relative to the module root.
153+ // determineInstallArg computes the argument passed to `go build` given we're changing cmd.Dir.
154+ // For remote modules, we keep the legacy behavior.
155+ // For local moduleURIs, we compute a relative path from the module root to the installPath
156+ // so the resulting arg is "." or "./sub/package".
157+ func determineInstallArg (installPath , moduleURI string , isLocal bool ) string {
158+ cleanInstallPath := filepath .Clean (installPath )
159+ cleanModuleURI := filepath .Clean (moduleURI )
160+
161+ // Local modules
162+ if isLocal {
163+ // 1 - If building the module root
164+ if cleanInstallPath == cleanModuleURI || cleanInstallPath == "." {
165+ return "."
166+ }
167+ // 2 - If installPath is inside the module root, return "./<rel>"
168+ if rel , err := filepath .Rel (cleanModuleURI , cleanInstallPath ); err == nil && rel != "" && ! strings .HasPrefix (rel , ".." ) {
169+ rel = filepath .ToSlash (rel )
170+ if rel == "." {
171+ return "."
172+ }
173+ return "./" + rel
174+ }
175+
176+ // 3 - If installPath is already relative to the module root, normalize "./" prefix
177+ if ! filepath .IsAbs (cleanInstallPath ) {
178+ cleanInstallPath = filepath .ToSlash (cleanInstallPath )
179+ if cleanInstallPath == "." || strings .HasPrefix (cleanInstallPath , "./" ) {
180+ return cleanInstallPath
181+ }
182+ return "./" + strings .TrimLeft (cleanInstallPath , "/" )
183+ }
184+
185+ // Absolute path outside module root: still give a relative-looking arg;
186+ // cmd.Dir will be set to module root so Go expects package paths like "./x/y".
187+ return "./" + filepath .ToSlash (strings .TrimLeft (cleanInstallPath , string (filepath .Separator )))
188+ }
129189
130- // Case 1: installPath is the moduleURI itself. Install the module root.
190+ // Remote modules
191+ // 1 - installPath is the module root itself.
131192 if installPath == moduleURI {
132193 return "."
133194 }
134195
135- // Case 2: installPath is a sub-package of moduleURI (e.g., "moduleURI/cmd/plugin") .
196+ // 2 - installPath is a sub-package of moduleURI.
136197 if after , ok := strings .CutPrefix (installPath , moduleURI + "/" ); ok {
137- // Extract the relative path and prefix with "./".
138- relativePath := after
139- cleanedRelativePath := strings .TrimLeft (relativePath , "/" ) // Handles "moduleURI///subpath"
140- if cleanedRelativePath == "" || cleanedRelativePath == "." { // Handles "moduleURI/" or "moduleURI/."
198+ cleanedRelativePath := strings .TrimLeft (after , "/" )
199+ if cleanedRelativePath == "" || cleanedRelativePath == "." {
141200 return "."
142201 }
143-
144- // cleanedRelativePath is like "cmd/plugin" or "sub/../pkg". Prepend "./".
145202 return "./" + cleanedRelativePath
146203 }
147204
148- // Case 3: installPath is not moduleURI and not a sub-package of moduleURI.
149- // Assumed to be:
150- // a) A path already relative to the module root (e.g., "cmd/plugin", "./cmd/plugin", ".").
151- // b) A full path to a different module (e.g., "github.com/other/mod").
152- // For (b), prefixing with "./" when cmd.Dir is set is problematic but replicates prior behavior if any.
153-
154- // Simple case
205+ // 3 - other inputs; normalize to a "./" path.
155206 if installPath == "." {
156207 return "."
157208 }
158-
159- // Already correctly formatted (e.g., "./cmd/plugin", "./sub/../pkg")
160209 if strings .HasPrefix (installPath , "./" ) {
161210 return installPath
162211 }
163-
164- // Needs "./" prefix. Handles "cmd/plugin", "/cmd/plugin", "github.com/other/mod".
165212 return "./" + strings .TrimLeft (installPath , "/" )
166213}
167214
168- // downloadAndInstallPlugin downloads and installs a single plugin
215+ // downloadAndInstallPlugin downloads (if remote) and builds the plugin.
216+ // For local moduleURIs (absolute or "./relative"), we skip network download,
217+ // ignore gitRef (with a log message), and build directly from the local dir.
169218func downloadAndInstallPlugin (pluginType string , pluginIdx int , plugin PluginDef , defaults DefaultsConfig ) error {
219+ pluginKey := fmt .Sprintf ("%s[%d]" , pluginType , pluginIdx )
170220 if ! isPluginEnabled (plugin ) {
171- log .Printf ("Skipping disabled plugin %s[%d] " , pluginType , pluginIdx )
221+ log .Printf ("%s - skipping disabled plugin" , pluginKey )
172222 return nil
173223 }
174224
175225 // Validate inputs
176226 if err := plugin .Validate (); err != nil {
177- return fmt .Errorf ("plugin input validation failed: %w" , err )
227+ return fmt .Errorf ("%s - plugin input validation failed: %w" , pluginKey , err )
178228 }
179229
180230 moduleURI := plugin .ModuleURI
181231 gitRef := plugin .GitRef
182232 installPath := plugin .InstallPath
183233
184- // Full module path with git reference
185- fullModulePath := moduleURI
186- if gitRef != "" {
187- fullModulePath = fmt .Sprintf ("%s@%s" , moduleURI , gitRef )
188- }
189-
190- log .Printf ("Installing plugin %s[%d] from %s" , pluginType , pluginIdx , fullModulePath )
191-
192- // Get GOPRIVATE environment variable
193234 goPrivate := os .Getenv ("GOPRIVATE" )
194235
195- // Download the module and get its directory
196- moduleDir , err := determineModuleDirectory (goPrivate , fullModulePath )
236+ // Determine the directory to run `go build` in.
237+ isLocal := filepath .IsAbs (moduleURI ) || strings .HasPrefix (moduleURI , "." + string (filepath .Separator ))
238+ moduleDir , err := func () (string , error ) {
239+ if isLocal {
240+ return determineModuleDirectoryLocal (pluginKey , moduleURI )
241+ }
242+ return determineModuleDirectoryRemote (pluginKey , moduleURI , gitRef , goPrivate )
243+ }()
197244 if err != nil {
198- return fmt .Errorf ("failed to determine module directory: %w" , err )
245+ return fmt .Errorf ("%s - failed to determine module directory: %w" , pluginKey , err )
246+ }
247+ if moduleDir == "" {
248+ return fmt .Errorf ("%s - empty module directory resolved" , pluginKey )
199249 }
200250
201- // Build env vars from defaults, environment variable, and plugin-specific settings
251+ log .Printf ("%s - installing plugin from %s" , pluginKey , moduleDir )
252+
253+ // Build env vars from defaults, environment variable, and plugin-specific settings.
202254 envVars := defaults .EnvVars
203255 if envEnvVars := os .Getenv ("CL_PLUGIN_ENVVARS" ); envEnvVars != "" {
204256 envVars = mergeOrReplaceEnvVars (envVars , strings .Fields (envEnvVars ))
205257 }
206-
207- // Merge plugin-specific env vars
208258 if len (plugin .EnvVars ) != 0 {
209259 envVars = mergeOrReplaceEnvVars (envVars , plugin .EnvVars )
210260 }
211261
212- // Install the plugin
213- {
214- installArg := determineInstallArg (installPath , moduleURI )
215-
216- binaryName := filepath .Base (installArg )
217- if binaryName == "." {
218- binaryName = filepath .Base (moduleURI )
219- }
220-
221- // Determine output directory
222- outputDir := os .Getenv ("GOBIN" )
223- if outputDir == "" {
224- gopath := os .Getenv ("GOPATH" )
225- if gopath == "" {
226- gopath = filepath .Join (os .Getenv ("HOME" ), "go" )
227- }
228- outputDir = filepath .Join (gopath , "bin" )
229- }
230-
231- outputPath := filepath .Join (outputDir , binaryName )
262+ // Compute build target relative to module root ('.' or './subpkg').
263+ installArg := determineInstallArg (installPath , moduleURI , isLocal )
232264
233- // Build goflags
234- goflags , err := determineGoFlags ( defaults . GoFlags , plugin . Flags )
235- if err != nil {
236- return fmt . Errorf ( "validation failed: %w" , err )
237- }
265+ // Derive output binary name. When arg is ".", use the module/repo (or local dir) name.
266+ binaryName := filepath . Base ( installArg )
267+ if binaryName == "." {
268+ binaryName = filepath . Base ( filepath . Clean ( moduleURI ) )
269+ }
238270
239- args := []string {"build" , "-o" , outputPath }
240- if len (goflags ) != 0 {
241- args = append (args , goflags ... )
271+ // Determine output directory (GOBIN, or GOPATH/bin, or $HOME/go/bin).
272+ outputDir := os .Getenv ("GOBIN" )
273+ if outputDir == "" {
274+ gopath := os .Getenv ("GOPATH" )
275+ if gopath == "" {
276+ gopath = filepath .Join (os .Getenv ("HOME" ), "go" )
242277 }
243- args = append (args , installArg )
278+ outputDir = filepath .Join (gopath , "bin" )
279+ }
280+ outputPath := filepath .Join (outputDir , binaryName )
244281
245- cmd := exec .Command ("go" , args ... )
246- cmd .Dir = moduleDir
247- cmd .Stdout = os .Stdout
248- cmd .Stderr = os .Stderr
282+ // Build goflags
283+ goflags , err := determineGoFlags (pluginKey , defaults .GoFlags , plugin .Flags )
284+ if err != nil {
285+ return fmt .Errorf ("%s - goflags validation failed: %w" , pluginKey , err )
286+ }
249287
250- // Start with all current environment variables
251- cmd .Env = os .Environ ()
288+ // Assemble `go build` command.
289+ args := []string {"build" , "-o" , outputPath }
290+ if len (goflags ) != 0 {
291+ args = append (args , goflags ... )
292+ }
293+ args = append (args , installArg )
252294
253- // Set GOPRIVATE environment variable if provided
254- if goPrivate != "" {
255- cmd .Env = mergeOrReplaceEnvVars ( cmd . Env , [] string { "GOPRIVATE=" + goPrivate })
256- }
295+ cmd := exec . Command ( "go" , args ... )
296+ cmd . Dir = moduleDir
297+ cmd .Stdout = os . Stdout
298+ cmd . Stderr = os . Stderr
257299
258- // Add/replace custom environment variables (e.g., GOOS, GOARCH, CGO_ENABLED)
259- cmd .Env = mergeOrReplaceEnvVars (cmd .Env , envVars )
300+ // Start with all current environment variables.
301+ cmd .Env = os .Environ ()
302+ if goPrivate != "" {
303+ cmd .Env = mergeOrReplaceEnvVars (cmd .Env , []string {"GOPRIVATE=" + goPrivate })
304+ }
305+ cmd .Env = mergeOrReplaceEnvVars (cmd .Env , envVars )
260306
261- log .Printf ("Running install command: go %s (in directory: %s)" , strings .Join (args , " " ), moduleDir )
307+ log .Printf ("%s - running install command: go %s (in directory: %s)" , pluginKey , strings .Join (args , " " ), moduleDir )
262308
263- if err := execCommand (cmd ); err != nil {
264- return fmt .Errorf ("failed to install plugin %s[%d]: %w" , pluginType , pluginIdx , err )
265- }
309+ if err := execCommand (cmd ); err != nil {
310+ return fmt .Errorf ("%s - failed to install plugin: %w" , pluginKey , err )
266311 }
267312
268313 return nil
269314}
270315
271- // writeBuildManifest writes installation artifacts to the specified file
316+ // writeBuildManifest writes installation artifacts to the specified file.
272317func writeBuildManifest (tasks []PluginInstallTask , outputFile string ) error {
273318 manifest := BuildManifest {
274319 BuildTime : time .Now ().UTC ().Format (time .RFC3339 ),
@@ -279,8 +324,7 @@ func writeBuildManifest(tasks []PluginInstallTask, outputFile string) error {
279324 for _ , task := range tasks {
280325 configPath := task .ConfigFile
281326 if ! filepath .IsAbs (configPath ) {
282- absPath , err := filepath .Abs (configPath )
283- if err == nil {
327+ if absPath , err := filepath .Abs (configPath ); err == nil {
284328 configPath = absPath
285329 }
286330 }
@@ -320,7 +364,7 @@ func writeBuildManifest(tasks []PluginInstallTask, outputFile string) error {
320364 return nil
321365}
322366
323- // installPlugins installs plugins concurrently using worker pool pattern
367+ // installPlugins installs plugins concurrently using a worker pool.
324368func installPlugins (tasks []PluginInstallTask , concurrency int , verbose bool , outputFile string ) error {
325369 if len (tasks ) == 0 {
326370 log .Println ("No enabled plugins found to install" )
@@ -329,6 +373,7 @@ func installPlugins(tasks []PluginInstallTask, concurrency int, verbose bool, ou
329373
330374 log .Printf ("Installing %d plugins with concurrency %d" , len (tasks ), concurrency )
331375
376+ // Optionally write the manifest first (so artifacts exist even if a build fails).
332377 if outputFile != "" {
333378 if err := writeBuildManifest (tasks , outputFile ); err != nil {
334379 return fmt .Errorf ("failed to write installation artifacts: %w" , err )
@@ -349,6 +394,7 @@ func installPlugins(tasks []PluginInstallTask, concurrency int, verbose bool, ou
349394 }
350395
351396 start := time .Now ()
397+
352398 err := downloadAndInstallPlugin (task .PluginType , 0 , task .Plugin , task .Defaults )
353399 duration := time .Since (start )
354400
@@ -407,7 +453,7 @@ func installPlugins(tasks []PluginInstallTask, concurrency int, verbose bool, ou
407453 return nil
408454}
409455
410- // setupOutputFile ensures the output directory exists
456+ // setupOutputFile ensures the output path is absolute (and its directory exists is handled elsewhere).
411457func setupOutputFile (outputFile string ) (string , error ) {
412458 if ! filepath .IsAbs (outputFile ) {
413459 wd , err := os .Getwd ()
0 commit comments