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", &copyBuilder{})
    25  	archive.Register("zip", archive.NewMultiBuilder(".zip", &copyBuilder{}, archive.NewBuilder("zip")))
    26  	archive.Register("tgz", archive.NewMultiBuilder(".tgz", &copyBuilder{}, archive.NewBuilder("tgz")))
    27  	archive.Register("tar.gz", archive.NewMultiBuilder(".tar.gz", &copyBuilder{}, archive.NewBuilder("tar.gz")))
    28  	archive.Register("tar", archive.NewMultiBuilder(".tar", &copyBuilder{}, archive.NewBuilder("tar")))
    29  	archive.Register("gz", archive.NewMultiBuilder(".gz", &copyBuilder{}, archive.NewBuilder("gz")))
    30  	archive.Register("txz", archive.NewMultiBuilder(".txz", &copyBuilder{}, archive.NewBuilder("txz")))
    31  	archive.Register("tar.xz", archive.NewMultiBuilder(".tar.xz", &copyBuilder{}, archive.NewBuilder("tar.xz")))
    32  	archive.Register("7z", archive.NewMultiBuilder(".7z", &copyBuilder{}, &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  }