gitee.com/h79/goutils@v1.22.10/build/build.go (about) 1 package build 2 3 import ( 4 "context" 5 "fmt" 6 "gitee.com/h79/goutils/common/archive" 7 "gitee.com/h79/goutils/common/file" 8 fileconfig "gitee.com/h79/goutils/common/file/config" 9 "gitee.com/h79/goutils/common/json" 10 "gitee.com/h79/goutils/common/logger" 11 "go.uber.org/zap" 12 "io" 13 "os" 14 "os/exec" 15 "path/filepath" 16 "strings" 17 ) 18 19 // Default builder instance. 20 var Default = &Builder{} 21 22 func init() { 23 _ = Default 24 archive.Register("copy", ©Builder{}) 25 archive.Register("zip", archive.NewMultiBuilder(".zip", ©Builder{}, archive.NewBuilder("zip"))) 26 archive.Register("tgz", archive.NewMultiBuilder(".tgz", ©Builder{}, archive.NewBuilder("tgz"))) 27 archive.Register("tar.gz", archive.NewMultiBuilder(".tar.gz", ©Builder{}, archive.NewBuilder("tar.gz"))) 28 archive.Register("tar", archive.NewMultiBuilder(".tar", ©Builder{}, archive.NewBuilder("tar"))) 29 archive.Register("gz", archive.NewMultiBuilder(".gz", ©Builder{}, archive.NewBuilder("gz"))) 30 archive.Register("txz", archive.NewMultiBuilder(".txz", ©Builder{}, archive.NewBuilder("txz"))) 31 archive.Register("tar.xz", archive.NewMultiBuilder(".tar.xz", ©Builder{}, archive.NewBuilder("tar.xz"))) 32 archive.Register("7z", archive.NewMultiBuilder(".7z", ©Builder{}, &e7zBuilder{})) 33 } 34 35 // Builder is golang builder. 36 type Builder struct{} 37 38 // WithDefaults sets the defaults for a golang build and returns it. 39 func (*Builder) WithDefaults(build Build) (Build, error) { 40 if build.GoBinary == "" { 41 build.GoBinary = "go" 42 } 43 if build.Command == "" { 44 build.Command = "build" 45 } 46 if build.Dir == "" { 47 build.Dir = "." 48 } 49 if build.Main == "" { 50 build.Main = "." 51 } 52 if len(build.Ldflags) == 0 { 53 build.Ldflags = []string{"-s -w -X main.version={{.Version}} -X main.CommitV={{.Commit}} -X main.DateV={{.BuildDate}} -X main.BranchV={{.Branch}} -X main.TagV={{.Tag}} -X main.appName={{.ProjectName}}"} 54 } 55 56 if len(build.Goos) == 0 { 57 build.Goos = []string{"linux", "darwin", "windows"} 58 } 59 if len(build.GoArch) == 0 { 60 build.GoArch = []string{"amd64", "arm64", "386"} 61 } 62 return build, nil 63 } 64 65 // Build builds a golang build. 66 func (*Builder) Build(ctx *Context, build Build, opts Options) error { 67 var venv []string 68 venv = append(venv, ctx.Env.Strings()...) 69 for _, e := range build.Details.Env { 70 ee, err := NewTemplate(ctx).WithEnvS(venv).Apply(e) 71 if err != nil { 72 return err 73 } 74 logger.I("builder", "env '%v' evaluated to '%v'", e, ee) 75 if ee != "" { 76 venv = append(venv, ee) 77 } 78 } 79 venv = append( 80 venv, 81 "GOOS="+opts.Goos, 82 "GOARCH="+opts.Goarch, 83 ) 84 85 cmd, err := buildGoBuildLine(ctx, build, build.Details, &opts, venv) 86 if err != nil { 87 return err 88 } 89 90 logger.I("builder", "build '%s' running", opts.Target) 91 logger.I("builder", "build cmd= '%v'", strings.Join(cmd, " ")) 92 if err = run(ctx, cmd, venv, build.Dir); err != nil { 93 logger.E("builder", "build '%s' failure, err= %s", opts.Target, err) 94 return fmt.Errorf("failed to build for %s: %w", opts.Target, err) 95 } 96 logger.I("builder", "build '%s' completed", opts.Target) 97 return nil 98 } 99 100 func (b *Builder) BuildHashFileName(ctx *Context, bit Bit, filename string) string { 101 temp := NewTemplate(ctx) 102 if filename == "" { 103 if !bit.Bit(HashNeedProject) || ctx.Config.ProjectName == "" { 104 temp.AddField(projectName, "") 105 } else { 106 temp.AddField(projectName, "_"+ctx.Config.ProjectName) 107 } 108 if !bit.Bit(HashNeedModel) || ctx.Config.Model == "" { 109 temp.AddField(model, "") 110 } else { 111 temp.AddField(model, "_"+ctx.Config.Model) 112 } 113 if !bit.Bit(HashNeedOs) || ctx.Config.Os == "" { 114 temp.AddField(osKey, "") 115 } else { 116 temp.AddField(osKey, "_"+ctx.Config.Os) 117 } 118 if !bit.Bit(HashNeedVersion) { 119 temp.AddField(version, "") 120 } else { 121 temp.AddField(version, "-"+ctx.Config.Version) 122 } 123 filename = "updates{{.ProjectName}}{{.Model}}{{.Os}}{{.Version}}.json" 124 } 125 filename, err := temp.Apply(filename) 126 if err != nil { 127 return "updates.json" 128 } 129 return filename 130 } 131 132 func (b *Builder) BuildTargetFileName(ctx *Context, bit Bit, filename string) string { 133 temp := NewTemplate(ctx) 134 if filename == "" { 135 filename = ctx.Config.ProjectName 136 if !bit.Bit(TargetNeedDate) { 137 temp.AddField(date, "") 138 } else { 139 filename += ".{{.BuildDate}}" 140 } 141 if !bit.Bit(TargetNeedModel) || ctx.Config.Model == "" { 142 temp.AddField(model, "") 143 } else { 144 filename += "_{{.Model}}" 145 } 146 if !bit.Bit(TargetNeedOs) || ctx.Config.Os == "" { 147 temp.AddField(osKey, "") 148 } else { 149 filename += "_{{.Os}}" 150 } 151 if !bit.Bit(TargetNeedVersion) { 152 temp.AddField(version, "") 153 } else { 154 filename += "-{{.Version}}" 155 } 156 } 157 filename, err := temp.Apply(filename) 158 if err != nil { 159 return ctx.Config.ProjectName 160 } 161 return filename 162 } 163 164 // Package 打包 165 func (b *Builder) Package(ctx *Context, build Build, out *PackOut) error { 166 logger.I("builder", "package '%s' running use '%s' format for '%s'", build.Target, build.Package.Format, build.Package.Model) 167 if build.Package.Format == "" { 168 build.Package.Format = "zip" 169 } 170 if build.Package.Dist == "" { 171 build.Package.Dist = "./build" 172 } 173 if build.Package.PackagePath == "" { 174 build.Package.PackagePath = "packages" 175 } 176 if build.Package.PublishPath == "" { 177 build.Package.PublishPath = "publishes" 178 } 179 out.Version = ctx.Config.Version 180 out.Path = strings.TrimSuffix(build.Package.Dist, "/") 181 182 build.Package.Dist = filepath.Join(out.Path, "dist") 183 build.Package.Parse() 184 185 if build.Package.packCompleted == nil { 186 build.Package.packCompleted = func(out *PackOut, pf *PackFile, completed bool) {} 187 } 188 if build.Package.BaseUrl != "" { 189 build.Package.BaseUrl = strings.TrimSuffix(build.Package.BaseUrl, "/") 190 build.Package.BaseUrl += "/" 191 } 192 out.PackagePath = filepath.Join(out.Path, build.Package.PackagePath) 193 out.PublishPath = filepath.Join(out.Path, build.Package.PublishPath) 194 out.Ext = archive.Ext(build.Package.Format) 195 if out.Ext != "" { 196 out.Name = b.BuildTargetFileName(ctx, build.Package.flag, build.Package.Target) 197 out.Target = filepath.Join(out.PackagePath, out.Name+out.Ext) 198 } else { 199 out.Name = "" 200 out.Target = "" 201 } 202 if build.Package.IsFlag(OutHashFile) { 203 build.Package.Hash = b.BuildHashFileName(ctx, build.Package.flag, build.Package.Hash) 204 out.Hash = filepath.Join(build.Target, build.Package.Hash) 205 } 206 logger.I("builder", "package info{dist: '%s', Path: '%s', target: '%s'}", build.Package.Dist, out.Path, out.Target) 207 if build.Package.IsFlag(OnlyPackOut) { 208 return nil 209 } 210 err := os.RemoveAll(build.Package.Dist) 211 err = os.MkdirAll(build.Package.Dist, 0777) 212 if err != nil { 213 logger.W("builder", "mkdir dist failure, dir= %s, err= %s", build.Package.Dist, err) 214 } 215 err = os.Mkdir(out.PackagePath, 0777) 216 if err != nil { 217 logger.W("builder", "mkdir packages failure, dir= %s, err= %s", out.PackagePath, err) 218 } 219 err = os.Mkdir(out.PublishPath, 0777) 220 if err != nil { 221 logger.W("builder", "mkdir publishes failure, dir= %s, err= %s", out.PublishPath, err) 222 } 223 err = b.buildPackage(ctx, &build, out) 224 if err != nil { 225 logger.E("builder", "package failure, err= %s", err) 226 _ = os.RemoveAll(build.Package.Dist) 227 return err 228 } 229 logger.I("builder", "package build completed") 230 return nil 231 } 232 233 func (b *Builder) buildPackage(ctx *Context, build *Build, out *PackOut) error { 234 var err error 235 var f *os.File = nil 236 if out.Target != "" { 237 _ = os.Remove(out.Target) 238 f, err = os.Create(out.Target) 239 if err != nil { 240 return err 241 } 242 } 243 defer func() { 244 if f != nil { 245 _ = f.Close() // nolint: errcheck 246 } 247 }() 248 if build.Package.IsFlag(NotArchive) { 249 //不需要压缩,只创建目录 250 return nil 251 } 252 archiveConfig := archive.Config{ 253 Format: build.Package.Format, 254 Path: build.Package.Dist, 255 File: out.Target, 256 } 257 ar, err := archive.New(f, archiveConfig) 258 if err != nil { 259 logger.E("builder", "create archive file failure, config= %#v, err: %s", archiveConfig, err) 260 return err 261 } 262 defer func(ar archive.Archive) { 263 err = ar.Close() 264 if err != nil { 265 logger.E("builder", "close archive err= %s", err) 266 } 267 }(ar) // nolint: errcheck 268 if build.Package.IsFlag(OutHashFile) { 269 err = os.Remove(out.Hash) 270 } 271 root := filepath.Join(build.Target, "/") 272 root = filepath.ToSlash(root) 273 var relativeName = func(name string) string { 274 return strings.TrimPrefix(filepath.ToSlash(name), root) 275 } 276 var destName = func(dest string) string { 277 var ot = "./" 278 if build.Package.IsFlag(ArchiveProject) { 279 ot = filepath.Join(ot, build.ProjectName) 280 } 281 if build.Package.IsFlag(ArchiveVersion) { 282 ot = filepath.Join(ot, ctx.Config.Version) 283 } 284 if ot != "./" { 285 dest = filepath.Join(ot, dest) 286 } 287 return dest 288 } 289 var packUrl = func(name string) string { 290 if build.Package.BaseUrl == "" { 291 return "" 292 } 293 return build.Package.BaseUrl + name 294 } 295 var pack = HashFile{ 296 Os: build.Package.Os, 297 Version: ctx.Config.Version, 298 Model: build.Package.Model, 299 ProductCode: build.Package.ProductCode, 300 ProductId: build.ProductId, ProjectName: build.ProjectName} 301 logger.I("builder", "archive config= %+v, target= '%s', root= '%s'", archiveConfig, build.Target, root) 302 depth := file.WithMaxDepth() 303 err = file.ReadDir(build.Target, &depth, func(name string, isDir bool, entry os.DirEntry) int { 304 baseName := entry.Name() 305 relative := relativeName(name) 306 if build.Package.Ignore(relative, baseName, isDir) { 307 //logger.W("builder", "ignore the file: %s, %s", relative, baseName) 308 return 1 //ignore 309 } 310 dest := destName(relative) 311 err = ar.Add(fileconfig.File{ 312 Info: fileconfig.FileInfo{Mode: 0777}, 313 Source: name, 314 Destination: dest, 315 Dir: isDir, 316 }, fileconfig.ReaderStreamFunc(func(r io.ReadSeeker) { 317 if !build.Package.IsFlag(OutHashFile) { 318 return 319 } 320 if _, err = r.Seek(0, io.SeekStart); err != nil { 321 return 322 } 323 324 pf := PackFile{Name: dest, Url: packUrl(baseName)} 325 pf.MD5, pf.Size = file.MD5Size(r) 326 build.Package.packCompleted(out, &pf, false) 327 328 pack.Packs = append(pack.Packs, pf) 329 })) 330 if err != nil { 331 logger.E("builder", "add the file to archive failure(%s=>%s), err= %s", name, dest, err) 332 return -1 333 } 334 return 0 335 }) 336 if err == nil { 337 if !build.Package.IsFlag(OutHashFile) { 338 return nil 339 } 340 uf := filepath.Join(build.Target, build.Package.Hash) 341 err = json.Write(uf, &pack, 0777) 342 if err != nil { 343 return fmt.Errorf("create hash file,err= %s", err) 344 } 345 if !build.Package.IsFlag(NeedArchiveHash) { 346 return nil 347 } 348 err = ar.Add(fileconfig.File{ 349 Info: fileconfig.FileInfo{Mode: 0777}, 350 Source: uf, 351 Destination: destName(build.Package.Hash), 352 }, fileconfig.ReaderStreamFunc(func(r io.ReadSeeker) { 353 if _, err = r.Seek(0, io.SeekStart); err != nil { 354 return 355 } 356 pf := PackFile{Name: destName(relativeName(uf)), Url: packUrl(build.Package.Hash)} 357 pf.MD5, pf.Size = file.MD5Size(r) 358 build.Package.packCompleted(out, &pf, true) 359 360 logger.I("builder", "hash file= '%+v'", pf) 361 })) 362 if err != nil { 363 return fmt.Errorf("archive hash file= '%s',err= %s", uf, err) 364 } 365 } 366 return err 367 } 368 369 func (*Builder) Clean(ctx *Context, build Build, options Options) error { 370 logger.I("builder", "clean running") 371 372 _ = os.Remove(filepath.Join(build.Target, "logs")) 373 374 if err := os.Remove(options.Path); err != nil { 375 logger.W("builder", "clean '%s' failure,err= %s", options.Path, err) 376 } 377 if build.Package.Dist == "" { 378 build.Package.Dist = "./build" 379 } 380 dir := strings.TrimSuffix(build.Package.Dist, "/") 381 if err := os.RemoveAll(dir); err != nil { 382 logger.W("builder", "clean '%s' failure, err= %s", dir, err) 383 return nil 384 } 385 logger.I("builder", "clean completed") 386 return nil 387 } 388 389 func buildGoBuildLine(ctx *Context, build Build, details Details, options *Options, env []string) ([]string, error) { 390 cmd := []string{build.GoBinary, build.Command} 391 392 // tags, ldflags, and buildmode, should only appear once, warning only to avoid a breaking change 393 validateUniqueFlags(details) 394 395 flags, err := processFlags(ctx, env, details.Flags, "") 396 if err != nil { 397 return cmd, err 398 } 399 cmd = append(cmd, flags...) 400 401 asmFlags, err := processFlags(ctx, env, details.AsmFlags, "-asmflags=") 402 if err != nil { 403 return cmd, err 404 } 405 cmd = append(cmd, asmFlags...) 406 407 gcFlags, err := processFlags(ctx, env, details.GcFlags, "-gcflags=") 408 if err != nil { 409 return cmd, err 410 } 411 cmd = append(cmd, gcFlags...) 412 413 // tags is not a repeatable flag 414 if len(details.Tags) > 0 { 415 tags, err := processFlags(ctx, env, details.Tags, "") 416 if err != nil { 417 return cmd, err 418 } 419 cmd = append(cmd, "-tags="+strings.Join(tags, ",")) 420 } 421 422 // ldflags is not a repeatable flag 423 if len(details.Ldflags) > 0 { 424 // flag prefix is skipped because ldflags need to output a single string 425 ldflags, err := processFlags(ctx, env, details.Ldflags, "") 426 if err != nil { 427 return cmd, err 428 } 429 // ldflags need to be single string in order to apply correctly 430 cmd = append(cmd, "-ldflags="+strings.Join(ldflags, " ")) 431 } 432 433 if details.Buildmode != "" { 434 cmd = append(cmd, "-buildmode="+details.Buildmode) 435 } 436 437 cmd = append(cmd, "-o", options.Path, build.Main) 438 return cmd, nil 439 } 440 441 func validateUniqueFlags(details Details) { 442 for _, flag := range details.Flags { 443 if strings.HasPrefix(flag, "-tags") && len(details.Tags) > 0 { 444 zap.L().Warn("builder", zap.String("flag", flag), zap.Any("tags", details.Tags)) 445 } 446 if strings.HasPrefix(flag, "-ldflags") && len(details.Ldflags) > 0 { 447 zap.L().Warn("builder", zap.String("flag", flag), zap.Any("ldflags", details.Ldflags)) 448 } 449 if strings.HasPrefix(flag, "-buildmode") && details.Buildmode != "" { 450 zap.L().Warn("builder", zap.String("flag", flag), zap.Any("buildmode", details.Buildmode)) 451 } 452 } 453 } 454 455 func processFlags(ctx *Context, env, flags []string, flagPrefix string) ([]string, error) { 456 processed := make([]string, 0, len(flags)) 457 for _, rawFlag := range flags { 458 flag, err := processFlag(ctx, env, rawFlag) 459 if err != nil { 460 return nil, err 461 } 462 processed = append(processed, flagPrefix+flag) 463 } 464 return processed, nil 465 } 466 467 func processFlag(ctx *Context, env []string, rawFlag string) (string, error) { 468 return NewTemplate(ctx).WithEnvS(env).Apply(rawFlag) 469 } 470 471 func run(ctx context.Context, command, env []string, dir string) error { 472 cmd := exec.CommandContext(ctx, command[0], command[1:]...) 473 cmd.Env = env 474 cmd.Dir = dir 475 out, err := cmd.CombinedOutput() 476 if err != nil { 477 return fmt.Errorf("%w: %s", err, string(out)) 478 } 479 return nil 480 }