github.com/hashicorp/packer@v1.14.3/scripts/generate-plugins.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 // Generate Plugins is a small program that updates the lists of plugins in 5 // command/plugin.go so they will be compiled into the main packer binary. 6 // 7 // See https://github.com/hashicorp/packer/pull/2608 for details. 8 package main 9 10 import ( 11 "fmt" 12 "go/ast" 13 "go/parser" 14 "go/token" 15 "log" 16 "os" 17 "path/filepath" 18 "sort" 19 "strings" 20 21 "golang.org/x/tools/imports" 22 ) 23 24 const target = "command/execute.go" 25 26 func main() { 27 wd, _ := os.Getwd() 28 if filepath.Base(wd) != "packer" { 29 log.Fatalf("This program must be invoked in the packer project root; in %s", wd) 30 } 31 32 // Collect all of the data we need about plugins we have in the project 33 builders, err := discoverBuilders() 34 if err != nil { 35 log.Fatalf("Failed to discover builders: %s", err) 36 } 37 38 provisioners, err := discoverProvisioners() 39 if err != nil { 40 log.Fatalf("Failed to discover provisioners: %s", err) 41 } 42 43 postProcessors, err := discoverPostProcessors() 44 if err != nil { 45 log.Fatalf("Failed to discover post processors: %s", err) 46 } 47 48 datasources, err := discoverDatasources() 49 if err != nil { 50 log.Fatalf("Failed to discover Datasources: %s", err) 51 } 52 53 // Do some simple code generation and templating 54 output := source 55 output = strings.Replace(output, "IMPORTS", makeImports(builders, provisioners, postProcessors, datasources), 1) 56 output = strings.Replace(output, "BUILDERS", makeMap("Builders", "Builder", builders), 1) 57 output = strings.Replace(output, "PROVISIONERS", makeMap("Provisioners", "Provisioner", provisioners), 1) 58 output = strings.Replace(output, "POSTPROCESSORS", makeMap("PostProcessors", "PostProcessor", postProcessors), 1) 59 output = strings.Replace(output, "DATASOURCES", makeMap("Datasources", "Datasource", datasources), 1) 60 61 // TODO sort the lists of plugins so we are not subjected to random OS ordering of the plugin lists 62 // TODO format the file 63 64 // Write our generated code to the command/plugin.go file 65 file, err := os.Create(target) 66 if err != nil { 67 log.Fatalf("Failed to open %s for writing: %s", target, err) 68 } 69 defer file.Close() 70 71 output = string(goFmt(target, []byte(output))) 72 73 _, err = file.WriteString(output) 74 if err != nil { 75 log.Fatalf("Failed writing to %s: %s", target, err) 76 } 77 78 log.Printf("Generated %s", target) 79 } 80 81 func goFmt(filename string, b []byte) []byte { 82 fb, err := imports.Process(filename, b, nil) 83 if err != nil { 84 log.Printf("formatting err: %v", err) 85 return b 86 } 87 return fb 88 } 89 90 type plugin struct { 91 Package string // This plugin's package name (iso) 92 PluginName string // Name of plugin (vmware-iso) 93 TypeName string // Type of plugin (builder) 94 Path string // Path relative to packer root (builder/vmware/iso) 95 ImportName string // PluginName+TypeName (vmwareisobuilder) 96 } 97 98 // makeMap creates a map named Name with type packer.Name that looks something 99 // like this: 100 // 101 // var Builders = map[string]packersdk.Builder{ 102 // "amazon-chroot": new(chroot.Builder), 103 // "amazon-ebs": new(ebs.Builder), 104 // "amazon-instance": new(instance.Builder), 105 func makeMap(varName, varType string, items []plugin) string { 106 output := "" 107 108 output += fmt.Sprintf("var %s = map[string]packersdk.%s{\n", varName, varType) 109 for _, item := range items { 110 output += fmt.Sprintf("\t\"%s\": new(%s.%s),\n", item.PluginName, item.ImportName, item.TypeName) 111 } 112 output += "}\n" 113 return output 114 } 115 116 func makeImports(builders, provisioners, postProcessors, Datasources []plugin) string { 117 plugins := []string{} 118 119 for _, builder := range builders { 120 plugins = append(plugins, fmt.Sprintf("\t%s \"github.com/hashicorp/packer/%s\"\n", builder.ImportName, filepath.ToSlash(builder.Path))) 121 } 122 123 for _, provisioner := range provisioners { 124 plugins = append(plugins, fmt.Sprintf("\t%s \"github.com/hashicorp/packer/%s\"\n", provisioner.ImportName, filepath.ToSlash(provisioner.Path))) 125 } 126 127 for _, postProcessor := range postProcessors { 128 plugins = append(plugins, fmt.Sprintf("\t%s \"github.com/hashicorp/packer/%s\"\n", postProcessor.ImportName, filepath.ToSlash(postProcessor.Path))) 129 } 130 131 for _, datasource := range Datasources { 132 plugins = append(plugins, fmt.Sprintf("\t%s \"github.com/hashicorp/packer/%s\"\n", datasource.ImportName, filepath.ToSlash(datasource.Path))) 133 } 134 135 // Make things pretty 136 sort.Strings(plugins) 137 138 return strings.Join(plugins, "") 139 } 140 141 // listDirectories recursively lists directories under the specified path 142 func listDirectories(path string) ([]string, error) { 143 names := []string{} 144 items, err := os.ReadDir(path) 145 if err != nil { 146 return names, err 147 } 148 149 for _, item := range items { 150 // We only want directories 151 if !item.IsDir() || 152 item.Name() == "common" { 153 continue 154 } 155 currentDir := filepath.Join(path, item.Name()) 156 names = append(names, currentDir) 157 158 // Do some recursion 159 subNames, err := listDirectories(currentDir) 160 if err == nil { 161 names = append(names, subNames...) 162 } 163 } 164 165 return names, nil 166 } 167 168 // deriveName determines the name of the plugin (what you'll see in a packer 169 // template) based on the filesystem path. We use two rules: 170 // 171 // Start with -> builder/virtualbox/iso 172 // 173 // 1. Strip the root -> virtualbox/iso 174 // 2. Switch slash / to dash - -> virtualbox-iso 175 func deriveName(root, full string) string { 176 short, _ := filepath.Rel(root, full) 177 bits := strings.Split(short, string(os.PathSeparator)) 178 return strings.Join(bits, "-") 179 } 180 181 // deriveImport will build a unique import identifier based on packageName and 182 // the result of deriveName() 183 // 184 // This will be something like -> virtualboxisobuilder 185 // 186 // Which is long, but deterministic and unique. 187 func deriveImport(typeName, derivedName string) string { 188 return strings.Replace(derivedName, "-", "", -1) + strings.ToLower(typeName) 189 } 190 191 // discoverTypesInPath searches for types of typeID in path and returns a list 192 // of plugins it finds. 193 func discoverTypesInPath(path, typeID string) ([]plugin, error) { 194 postProcessors := []plugin{} 195 196 dirs, err := listDirectories(path) 197 if err != nil { 198 return postProcessors, err 199 } 200 201 for _, dir := range dirs { 202 fset := token.NewFileSet() 203 goPackages, err := parser.ParseDir(fset, dir, nil, parser.AllErrors) 204 if err != nil { 205 return postProcessors, fmt.Errorf("Failed parsing directory %s: %s", dir, err) 206 } 207 208 for _, goPackage := range goPackages { 209 ast.PackageExports(goPackage) 210 ast.Inspect(goPackage, func(n ast.Node) bool { 211 switch x := n.(type) { 212 case *ast.TypeSpec: 213 if x.Name.Name == typeID { 214 derivedName := deriveName(path, dir) 215 postProcessors = append(postProcessors, plugin{ 216 Package: goPackage.Name, 217 PluginName: derivedName, 218 ImportName: deriveImport(x.Name.Name, derivedName), 219 TypeName: x.Name.Name, 220 Path: dir, 221 }) 222 // The AST stops parsing when we return false. Once we 223 // find the symbol we want we can stop parsing. 224 225 // DEBUG: 226 // fmt.Printf("package %#v\n", goPackage) 227 return false 228 } 229 } 230 return true 231 }) 232 } 233 } 234 235 return postProcessors, nil 236 } 237 238 func discoverBuilders() ([]plugin, error) { 239 path := "./builder" 240 typeID := "Builder" 241 return discoverTypesInPath(path, typeID) 242 } 243 244 func discoverDatasources() ([]plugin, error) { 245 path := "./datasource" 246 typeID := "Datasource" 247 return discoverTypesInPath(path, typeID) 248 } 249 250 func discoverProvisioners() ([]plugin, error) { 251 path := "./provisioner" 252 typeID := "Provisioner" 253 return discoverTypesInPath(path, typeID) 254 } 255 256 func discoverPostProcessors() ([]plugin, error) { 257 path := "./post-processor" 258 typeID := "PostProcessor" 259 return discoverTypesInPath(path, typeID) 260 } 261 262 const source = `// 263 // This file is automatically generated by scripts/generate-plugins.go -- Do not edit! 264 // 265 266 package command 267 268 import ( 269 "flag" 270 "fmt" 271 "regexp" 272 "strings" 273 274 "github.com/hashicorp/packer/packer" 275 packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 276 "github.com/hashicorp/packer-plugin-sdk/plugin" 277 "github.com/hashicorp/packer-plugin-sdk/rpc" 278 279 IMPORTS 280 ) 281 282 type ExecuteCommand struct { 283 Meta 284 } 285 286 BUILDERS 287 288 PROVISIONERS 289 290 POSTPROCESSORS 291 292 DATASOURCES 293 294 var pluginRegexp = regexp.MustCompile("packer-(builder|post-processor|provisioner|datasource)-(.+)") 295 296 type ExecuteArgs struct { 297 UseProtobuf bool 298 CommandType string 299 } 300 301 func (ea *ExecuteArgs) AddFlagSets(flags *flag.FlagSet) { 302 flags.BoolVar(&ea.UseProtobuf, "protobuf", false, "Use protobuf for serialising data over the wire instead of gob") 303 } 304 305 func (c *ExecuteCommand) ParseArgs(args []string) (*ExecuteArgs, int) { 306 var cfg ExecuteArgs 307 flags := c.Meta.FlagSet("") 308 flags.Usage = func() { c.Ui.Say(c.Help()) } 309 cfg.AddFlagSets(flags) 310 if err := flags.Parse(args); err != nil { 311 return &cfg, 1 312 } 313 314 args = flags.Args() 315 if len(args) != 1 { 316 flags.Usage() 317 return &cfg, 1 318 } 319 cfg.CommandType = args[0] 320 return &cfg, 0 321 } 322 323 func (c *ExecuteCommand) Run(args []string) int { 324 cfg, ret := c.ParseArgs(args) 325 if ret != 0 { 326 return ret 327 } 328 329 return c.RunContext(cfg) 330 } 331 332 333 func (c *ExecuteCommand) RunContext(args *ExecuteArgs) int { 334 // Plugin will match something like "packer-builder-amazon-ebs" 335 parts := pluginRegexp.FindStringSubmatch(args.CommandType) 336 if len(parts) != 3 { 337 c.Ui.Error(c.Help()) 338 return 1 339 } 340 pluginType := parts[1] // capture group 1 (builder|post-processor|provisioner) 341 pluginName := parts[2] // capture group 2 (.+) 342 343 server, err := plugin.Server() 344 if err != nil { 345 c.Ui.Error(fmt.Sprintf("Error starting plugin server: %s", err)) 346 return 1 347 } 348 349 if args.UseProtobuf { 350 server.UseProto = true 351 } 352 353 switch pluginType { 354 case "builder": 355 builder, found := Builders[pluginName] 356 if !found { 357 c.Ui.Error(fmt.Sprintf("Could not load builder: %s", pluginName)) 358 return 1 359 } 360 server.RegisterBuilder(builder) 361 case "provisioner": 362 provisioner, found := Provisioners[pluginName] 363 if !found { 364 c.Ui.Error(fmt.Sprintf("Could not load provisioner: %s", pluginName)) 365 return 1 366 } 367 server.RegisterProvisioner(provisioner) 368 case "post-processor": 369 postProcessor, found := PostProcessors[pluginName] 370 if !found { 371 c.Ui.Error(fmt.Sprintf("Could not load post-processor: %s", pluginName)) 372 return 1 373 } 374 server.RegisterPostProcessor(postProcessor) 375 case "datasource": 376 datasource, found := Datasources[pluginName] 377 if !found { 378 c.Ui.Error(fmt.Sprintf("Could not load datasource: %s", pluginName)) 379 return 1 380 } 381 server.RegisterDatasource(datasource) 382 } 383 384 server.Serve() 385 386 return 0 387 } 388 389 func (*ExecuteCommand) Help() string { 390 helpText := ` + "`" + ` 391 Usage: packer execute [options] PLUGIN 392 393 Runs an internally-compiled version of a plugin from the packer binary. 394 395 NOTE: this is an internal command and you should not call it yourself. 396 397 Options: 398 399 --protobuf: use protobuf for serialising data over-the-wire instead of gob. 400 ` + "`" + ` 401 402 return strings.TrimSpace(helpText) 403 } 404 405 func (c *ExecuteCommand) Synopsis() string { 406 return "internal plugin command" 407 } 408 `