github.com/pojntfx/hydrapp/hydrapp@v0.0.0-20240516002902-d08759d6ca9f/cmd/build.go (about)

     1  package cmd
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/base64"
     7  	"errors"
     8  	"log"
     9  	"os"
    10  	"os/signal"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strings"
    14  	"sync"
    15  	"syscall"
    16  	"time"
    17  
    18  	"github.com/docker/docker/api/types"
    19  	"github.com/docker/docker/client"
    20  	"github.com/go-git/go-git/v5"
    21  	"github.com/go-git/go-git/v5/plumbing"
    22  	"github.com/pojntfx/hydrapp/hydrapp/pkg/builders"
    23  	"github.com/pojntfx/hydrapp/hydrapp/pkg/builders/apk"
    24  	"github.com/pojntfx/hydrapp/hydrapp/pkg/builders/binaries"
    25  	"github.com/pojntfx/hydrapp/hydrapp/pkg/builders/deb"
    26  	"github.com/pojntfx/hydrapp/hydrapp/pkg/builders/dmg"
    27  	"github.com/pojntfx/hydrapp/hydrapp/pkg/builders/docs"
    28  	"github.com/pojntfx/hydrapp/hydrapp/pkg/builders/flatpak"
    29  	"github.com/pojntfx/hydrapp/hydrapp/pkg/builders/msi"
    30  	"github.com/pojntfx/hydrapp/hydrapp/pkg/builders/rpm"
    31  	"github.com/pojntfx/hydrapp/hydrapp/pkg/builders/tests"
    32  	"github.com/pojntfx/hydrapp/hydrapp/pkg/config"
    33  	"github.com/pojntfx/hydrapp/hydrapp/pkg/secrets"
    34  	"github.com/pojntfx/hydrapp/hydrapp/pkg/utils"
    35  	"github.com/spf13/cobra"
    36  	"github.com/spf13/viper"
    37  	"gopkg.in/yaml.v2"
    38  )
    39  
    40  const (
    41  	configFlag      = "config"
    42  	pullFlag        = "pull"
    43  	tagFlag         = "tag"
    44  	concurrencyFlag = "concurrency"
    45  	ejectFlag       = "eject"
    46  	overwriteFlag   = "overwrite"
    47  	srcFlag         = "src"
    48  	dstFlag         = "dst"
    49  	excludeFlag     = "exclude"
    50  
    51  	javaKeystoreFlag = "java-keystore"
    52  
    53  	pgpKeyFlag   = "pgp-key"
    54  	pgpKeyIDFlag = "pgp-key-id"
    55  
    56  	branchIDFlag        = "branch-id"
    57  	branchNameFlag      = "branch-name"
    58  	branchTimestampFlag = "branch-timestamp"
    59  )
    60  
    61  func checkIfSkip(exclude string, platform, architecture string) (bool, error) {
    62  	if strings.TrimSpace(exclude) == "" {
    63  		return false, nil
    64  	}
    65  
    66  	skip, err := regexp.MatchString(exclude, platform+"/"+architecture)
    67  	if err != nil {
    68  		return false, err
    69  	}
    70  
    71  	if skip {
    72  		log.Printf("Skipping %v/%v (platform or architecture matched the provided regex)", platform, architecture)
    73  
    74  		return true, nil
    75  	}
    76  
    77  	return false, nil
    78  }
    79  
    80  var buildCmd = &cobra.Command{
    81  	Use:     "build",
    82  	Aliases: []string{"b"},
    83  	Short:   "Build a hydrapp project",
    84  	RunE: func(cmd *cobra.Command, args []string) error {
    85  		if err := viper.BindPFlags(cmd.PersistentFlags()); err != nil {
    86  			return err
    87  		}
    88  
    89  		ctx, cancel := context.WithCancel(context.Background())
    90  		defer cancel()
    91  
    92  		configFile, err := os.Open(viper.GetString(configFlag))
    93  		if err != nil {
    94  			return err
    95  		}
    96  		defer configFile.Close()
    97  
    98  		cfg, err := config.Parse(configFile)
    99  		if err != nil {
   100  			return err
   101  		}
   102  
   103  		var (
   104  			branchID        = viper.GetString(branchIDFlag)
   105  			branchName      = viper.GetString(branchNameFlag)
   106  			branchTimestamp = time.Unix(viper.GetInt64(branchTimestampFlag), 0)
   107  		)
   108  		if !(viper.IsSet(branchIDFlag) && viper.IsSet(branchNameFlag) && viper.IsSet(branchTimestampFlag)) {
   109  			repo, err := git.PlainOpen(viper.GetString(srcFlag))
   110  			if err != nil && !errors.Is(err, git.ErrRepositoryNotExists) { // If source directory is not a Git repository, use provided flags
   111  				return err
   112  			} else if err == nil {
   113  				headRef, err := repo.Head()
   114  				if err != nil {
   115  					return err
   116  				}
   117  
   118  				headCommit, err := repo.CommitObject(headRef.Hash())
   119  				if err != nil {
   120  					return err
   121  				}
   122  
   123  				tags, err := repo.Tags()
   124  				if err != nil {
   125  					return err
   126  				}
   127  
   128  				isTag := false
   129  				if err := tags.ForEach(func(r *plumbing.Reference) error {
   130  					if r.Hash() == headCommit.Hash {
   131  						isTag = true
   132  					}
   133  
   134  					return nil
   135  				}); err != nil {
   136  					return err
   137  				}
   138  
   139  				if isTag {
   140  					if !viper.IsSet(branchIDFlag) {
   141  						branchID = ""
   142  					}
   143  
   144  					if !viper.IsSet(branchNameFlag) {
   145  						branchName = ""
   146  					}
   147  				} else {
   148  					if !viper.IsSet(branchIDFlag) {
   149  						branchID = headRef.Name().Short()
   150  					}
   151  
   152  					if !viper.IsSet(branchNameFlag) {
   153  						branchName = utils.Capitalize(branchID)
   154  					}
   155  				}
   156  
   157  				if !viper.IsSet(branchTimestampFlag) {
   158  					branchTimestamp = headCommit.Author.When
   159  				}
   160  			}
   161  		}
   162  
   163  		var (
   164  			javaKeystore            []byte
   165  			javaKeystorePassword    string
   166  			javaCertificatePassword string
   167  
   168  			pgpKey         []byte
   169  			pgpKeyPassword string
   170  			pgpKeyID       string
   171  		)
   172  		if !viper.GetBool(ejectFlag) {
   173  			javaKeystorePassword = viper.GetString(javaKeystorePasswordFlag)
   174  			javaCertificatePassword = viper.GetString(javaCertificatePasswordFlag)
   175  
   176  			pgpKeyPassword = viper.GetString(pgpKeyPasswordFlag)
   177  			pgpKeyID = viper.GetString(pgpKeyIDFlag)
   178  
   179  			var scs secrets.Root
   180  			if strings.TrimSpace(viper.GetString(javaKeystoreFlag)) == "" &&
   181  				strings.TrimSpace(javaKeystorePassword) == "" &&
   182  				strings.TrimSpace(javaCertificatePassword) == "" &&
   183  
   184  				strings.TrimSpace(viper.GetString(pgpKeyFlag)) == "" &&
   185  				strings.TrimSpace(pgpKeyPassword) == "" &&
   186  				strings.TrimSpace(pgpKeyID) == "" {
   187  				secretsFile, err := os.Open(viper.GetString(secretsFlag))
   188  				if err == nil {
   189  					defer secretsFile.Close()
   190  
   191  					s, err := secrets.Parse(secretsFile)
   192  					if err != nil {
   193  						return err
   194  					}
   195  					scs = *s
   196  				} else {
   197  					if !errors.Is(err, os.ErrNotExist) {
   198  						return err
   199  					}
   200  
   201  					keystorePassword, err := secrets.GeneratePassword(32)
   202  					if err != nil {
   203  						return err
   204  					}
   205  
   206  					certificatePassword, err := secrets.GeneratePassword(32)
   207  					if err != nil {
   208  						return err
   209  					}
   210  
   211  					keystoreBuf := &bytes.Buffer{}
   212  					if err := secrets.GenerateKeystore(
   213  						keystorePassword,
   214  						certificatePassword,
   215  						fullNameDefault,
   216  						fullNameDefault,
   217  						certificateValidityDefault,
   218  						javaRSABitsDefault,
   219  						keystoreBuf,
   220  					); err != nil {
   221  						return err
   222  					}
   223  
   224  					pgpPassword, err := secrets.GeneratePassword(32)
   225  					if err != nil {
   226  						return err
   227  					}
   228  
   229  					pgpKey, pgpKeyID, err := secrets.GeneratePGPKey(
   230  						fullNameDefault,
   231  						emailDefault,
   232  						pgpPassword,
   233  					)
   234  					if err != nil {
   235  						return err
   236  					}
   237  
   238  					scs = secrets.Root{
   239  						JavaSecrets: secrets.JavaSecrets{
   240  							Keystore:            keystoreBuf.Bytes(),
   241  							KeystorePassword:    keystorePassword,
   242  							CertificatePassword: certificatePassword,
   243  						},
   244  						PGPSecrets: secrets.PGPSecrets{
   245  							Key:         pgpKey,
   246  							KeyID:       pgpKeyID,
   247  							KeyPassword: pgpPassword,
   248  						},
   249  					}
   250  
   251  					if err := os.MkdirAll(filepath.Dir(viper.GetString(secretsFlag)), os.ModePerm); err != nil {
   252  						return err
   253  					}
   254  
   255  					out, err := os.OpenFile(viper.GetString(secretsFlag), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm)
   256  					if err != nil {
   257  						return err
   258  					}
   259  					defer out.Close()
   260  
   261  					if err := yaml.NewEncoder(out).Encode(scs); err != nil {
   262  						return err
   263  					}
   264  				}
   265  			}
   266  
   267  			if strings.TrimSpace(viper.GetString(javaKeystoreFlag)) == "" {
   268  				javaKeystore = scs.JavaSecrets.Keystore
   269  			} else {
   270  				javaKeystore, err = os.ReadFile(viper.GetString(javaKeystoreFlag))
   271  				if err != nil {
   272  					return err
   273  				}
   274  			}
   275  
   276  			if strings.TrimSpace(javaKeystorePassword) == "" {
   277  				javaKeystorePassword = base64.StdEncoding.EncodeToString([]byte(scs.JavaSecrets.KeystorePassword))
   278  			}
   279  
   280  			if strings.TrimSpace(javaCertificatePassword) == "" {
   281  				javaCertificatePassword = base64.StdEncoding.EncodeToString([]byte(scs.JavaSecrets.CertificatePassword))
   282  			}
   283  
   284  			if strings.TrimSpace(viper.GetString(pgpKeyFlag)) == "" {
   285  				pgpKey = []byte(scs.PGPSecrets.Key)
   286  			} else {
   287  				pgpKey, err = os.ReadFile(viper.GetString(pgpKeyFlag))
   288  				if err != nil {
   289  					return err
   290  				}
   291  			}
   292  
   293  			if strings.TrimSpace(pgpKeyPassword) == "" {
   294  				pgpKeyPassword = base64.StdEncoding.EncodeToString([]byte(scs.PGPSecrets.KeyPassword))
   295  			}
   296  
   297  			if strings.TrimSpace(pgpKeyID) == "" {
   298  				pgpKeyID = base64.StdEncoding.EncodeToString([]byte(scs.PGPSecrets.KeyID))
   299  			}
   300  		}
   301  
   302  		licenseText, err := os.ReadFile(filepath.Join(filepath.Dir(viper.GetString(configFlag)), "LICENSE"))
   303  		if err != nil {
   304  			return err
   305  		}
   306  
   307  		cli, err := client.NewClientWithOpts(client.FromEnv)
   308  		if err != nil {
   309  			return err
   310  		}
   311  		defer cli.Close()
   312  
   313  		// See https://github.com/rancher/rke/issues/1711#issuecomment-578382159
   314  		cli.NegotiateAPIVersion(ctx)
   315  
   316  		handleID := func(id string) {
   317  			s := make(chan os.Signal, 1)
   318  			signal.Notify(s, os.Interrupt, syscall.SIGTERM)
   319  
   320  			go func() {
   321  				<-s
   322  
   323  				log.Println("Gracefully shutting down")
   324  
   325  				go func() {
   326  					<-s
   327  
   328  					log.Println("Forcing shutdown")
   329  
   330  					os.Exit(1)
   331  				}()
   332  
   333  				if err := cli.ContainerRemove(ctx, id, types.ContainerRemoveOptions{
   334  					Force: true,
   335  				}); err != nil {
   336  					panic(err)
   337  				}
   338  			}()
   339  		}
   340  
   341  		bdrs := []builders.Builder{}
   342  
   343  		for _, c := range cfg.DEB {
   344  			skip, err := checkIfSkip(viper.GetString(excludeFlag), "deb", c.Architecture)
   345  			if err != nil {
   346  				return err
   347  			}
   348  
   349  			if skip {
   350  				continue
   351  			}
   352  
   353  			bdrs = append(
   354  				bdrs,
   355  				deb.NewBuilder(
   356  					ctx,
   357  					cli,
   358  
   359  					deb.Image+":"+viper.GetString(tagFlag),
   360  					viper.GetBool(pullFlag),
   361  					viper.GetString(srcFlag),
   362  					filepath.Join(viper.GetString(dstFlag), c.Path),
   363  					handleID,
   364  					os.Stdout,
   365  					"icon.png",
   366  					cfg.App.ID,
   367  					pgpKey,
   368  					pgpKeyPassword,
   369  					pgpKeyID,
   370  					cfg.App.BaseURL+"/"+c.Path,
   371  					c.OS,
   372  					c.Distro,
   373  					c.Mirrorsite,
   374  					c.Components,
   375  					c.Debootstrapopts,
   376  					c.Architecture,
   377  					cfg.Releases,
   378  					cfg.App.Description,
   379  					cfg.App.Summary,
   380  					cfg.App.Homepage,
   381  					cfg.App.Git,
   382  					c.Packages,
   383  					cfg.App.License,
   384  					string(licenseText),
   385  					cfg.App.Name,
   386  					viper.GetBool(overwriteFlag),
   387  					branchID,
   388  					branchName,
   389  					branchTimestamp,
   390  					cfg.Go.Main,
   391  					cfg.Go.Flags,
   392  					cfg.Go.Generate,
   393  				),
   394  			)
   395  		}
   396  
   397  		if strings.TrimSpace(cfg.DMG.Path) != "" {
   398  			skip, err := checkIfSkip(viper.GetString(excludeFlag), "dmg", "")
   399  			if err != nil {
   400  				return err
   401  			}
   402  
   403  			if !skip {
   404  				bdrs = append(
   405  					bdrs,
   406  					dmg.NewBuilder(
   407  						ctx,
   408  						cli,
   409  
   410  						dmg.Image+":"+viper.GetString(tagFlag),
   411  						viper.GetBool(pullFlag),
   412  						viper.GetString(srcFlag),
   413  						filepath.Join(viper.GetString(dstFlag), cfg.DMG.Path),
   414  						handleID,
   415  						os.Stdout,
   416  						"icon.png",
   417  						cfg.App.ID,
   418  						cfg.App.Name,
   419  						pgpKey,
   420  						pgpKeyPassword,
   421  						cfg.DMG.Packages,
   422  						cfg.Releases,
   423  						viper.GetBool(overwriteFlag),
   424  						branchID,
   425  						branchName,
   426  						branchTimestamp,
   427  						cfg.Go.Main,
   428  						cfg.Go.Flags,
   429  						cfg.Go.Generate,
   430  					),
   431  				)
   432  			}
   433  		}
   434  
   435  		for _, c := range cfg.Flatpak {
   436  			skip, err := checkIfSkip(viper.GetString(excludeFlag), "flatpak", c.Architecture)
   437  			if err != nil {
   438  				return err
   439  			}
   440  
   441  			if skip {
   442  				continue
   443  			}
   444  
   445  			bdrs = append(
   446  				bdrs,
   447  				flatpak.NewBuilder(
   448  					ctx,
   449  					cli,
   450  
   451  					flatpak.Image+":"+viper.GetString(tagFlag),
   452  					viper.GetBool(pullFlag),
   453  					viper.GetString(srcFlag),
   454  					filepath.Join(viper.GetString(dstFlag), c.Path),
   455  					handleID,
   456  					os.Stdout,
   457  					"icon.png",
   458  					cfg.App.ID,
   459  					pgpKey,
   460  					pgpKeyPassword,
   461  					pgpKeyID,
   462  					cfg.App.BaseURL+"/"+c.Path,
   463  					c.Architecture,
   464  					cfg.App.Name,
   465  					cfg.App.Description,
   466  					cfg.App.Summary,
   467  					cfg.App.License,
   468  					cfg.App.Homepage,
   469  					cfg.Releases,
   470  					viper.GetBool(overwriteFlag),
   471  					branchID,
   472  					branchName,
   473  					cfg.Go.Main,
   474  					cfg.Go.Flags,
   475  					cfg.Go.Generate,
   476  				),
   477  			)
   478  		}
   479  
   480  		for _, c := range cfg.MSI {
   481  			skip, err := checkIfSkip(viper.GetString(excludeFlag), "msi", c.Architecture)
   482  			if err != nil {
   483  				return err
   484  			}
   485  
   486  			if skip {
   487  				continue
   488  			}
   489  
   490  			bdrs = append(
   491  				bdrs,
   492  				msi.NewBuilder(
   493  					ctx,
   494  					cli,
   495  
   496  					msi.Image+":"+viper.GetString(tagFlag),
   497  					viper.GetBool(pullFlag),
   498  					viper.GetString(srcFlag),
   499  					filepath.Join(viper.GetString(dstFlag), c.Path),
   500  					handleID,
   501  					os.Stdout,
   502  					"icon.png",
   503  					cfg.App.ID,
   504  					cfg.App.Name,
   505  					pgpKey,
   506  					pgpKeyPassword,
   507  					c.Architecture,
   508  					c.Packages,
   509  					cfg.Releases,
   510  					viper.GetBool(overwriteFlag),
   511  					branchID,
   512  					branchName,
   513  					branchTimestamp,
   514  					cfg.Go.Main,
   515  					cfg.Go.Flags,
   516  					c.Include,
   517  					cfg.Go.Generate,
   518  				),
   519  			)
   520  		}
   521  
   522  		for _, c := range cfg.RPM {
   523  			skip, err := checkIfSkip(viper.GetString(excludeFlag), "rpm", c.Architecture)
   524  			if err != nil {
   525  				return err
   526  			}
   527  
   528  			if skip {
   529  				continue
   530  			}
   531  
   532  			bdrs = append(
   533  				bdrs,
   534  				rpm.NewBuilder(
   535  					ctx,
   536  					cli,
   537  
   538  					rpm.Image+":"+viper.GetString(tagFlag),
   539  					viper.GetBool(pullFlag),
   540  					viper.GetString(srcFlag),
   541  					filepath.Join(viper.GetString(dstFlag), c.Path),
   542  					handleID,
   543  					os.Stdout,
   544  					"icon.png",
   545  					cfg.App.ID,
   546  					pgpKey,
   547  					pgpKeyPassword,
   548  					pgpKeyID,
   549  					cfg.App.BaseURL+"/"+c.Path,
   550  					c.Distro,
   551  					c.Architecture,
   552  					c.Trailer,
   553  					cfg.App.Name,
   554  					cfg.App.Description,
   555  					cfg.App.Summary,
   556  					cfg.App.Homepage,
   557  					cfg.App.License,
   558  					cfg.Releases,
   559  					c.Packages,
   560  					viper.GetBool(overwriteFlag),
   561  					branchID,
   562  					branchName,
   563  					branchTimestamp,
   564  					cfg.Go.Main,
   565  					cfg.Go.Flags,
   566  					cfg.Go.Generate,
   567  				),
   568  			)
   569  		}
   570  
   571  		if strings.TrimSpace(cfg.APK.Path) != "" {
   572  			skip, err := checkIfSkip(viper.GetString(excludeFlag), "apk", "")
   573  			if err != nil {
   574  				return err
   575  			}
   576  
   577  			if !skip {
   578  				bdrs = append(
   579  					bdrs,
   580  					apk.NewBuilder(
   581  						ctx,
   582  						cli,
   583  
   584  						apk.Image+":"+viper.GetString(tagFlag),
   585  						viper.GetBool(pullFlag),
   586  						viper.GetString(srcFlag),
   587  						filepath.Join(viper.GetString(dstFlag), cfg.APK.Path),
   588  						handleID,
   589  						os.Stdout,
   590  						cfg.App.ID,
   591  						javaKeystore,
   592  						javaKeystorePassword,
   593  						javaCertificatePassword,
   594  						pgpKey,
   595  						pgpKeyPassword,
   596  						cfg.App.BaseURL+"/"+cfg.APK.Path,
   597  						cfg.App.Name,
   598  						cfg.Releases,
   599  						viper.GetBool(overwriteFlag),
   600  						branchID,
   601  						branchName,
   602  						branchTimestamp,
   603  						cfg.Go.Main,
   604  						cfg.Go.Flags,
   605  						cfg.Go.Generate,
   606  					),
   607  				)
   608  			}
   609  		}
   610  
   611  		if strings.TrimSpace(cfg.Binaries.Path) != "" {
   612  			skip, err := checkIfSkip(viper.GetString(excludeFlag), "binaries", "")
   613  			if err != nil {
   614  				return err
   615  			}
   616  
   617  			if !skip {
   618  				bdrs = append(
   619  					bdrs,
   620  					binaries.NewBuilder(
   621  						ctx,
   622  						cli,
   623  
   624  						binaries.Image+":"+viper.GetString(tagFlag),
   625  						viper.GetBool(pullFlag),
   626  						viper.GetString(srcFlag),
   627  						filepath.Join(viper.GetString(dstFlag), cfg.Binaries.Path),
   628  						handleID,
   629  						os.Stdout,
   630  						cfg.App.ID,
   631  						pgpKey,
   632  						pgpKeyPassword,
   633  						cfg.App.Name,
   634  						branchID,
   635  						branchName,
   636  						branchTimestamp,
   637  						cfg.Go.Main,
   638  						cfg.Go.Flags,
   639  						cfg.Go.Generate,
   640  						cfg.Binaries.Exclude,
   641  						cfg.Binaries.Packages,
   642  					),
   643  				)
   644  			}
   645  		}
   646  
   647  		if strings.TrimSpace(cfg.Go.Tests) != "" {
   648  			skip, err := checkIfSkip(viper.GetString(excludeFlag), "tests", "")
   649  			if err != nil {
   650  				return err
   651  			}
   652  
   653  			if !skip {
   654  				bdrs = append(
   655  					bdrs,
   656  					tests.NewBuilder(
   657  						ctx,
   658  						cli,
   659  
   660  						cfg.Go.Image,
   661  						viper.GetBool(pullFlag),
   662  						viper.GetString(srcFlag),
   663  						"",
   664  						handleID,
   665  						os.Stdout,
   666  						cfg.Go.Flags,
   667  						cfg.Go.Generate,
   668  						cfg.Go.Tests,
   669  					),
   670  				)
   671  			}
   672  		}
   673  
   674  		if strings.TrimSpace(cfg.Docs.Path) != "" {
   675  			skip, err := checkIfSkip(viper.GetString(excludeFlag), "docs", "")
   676  			if err != nil {
   677  				return err
   678  			}
   679  
   680  			if !skip {
   681  				bdrs = append(
   682  					bdrs,
   683  					docs.NewBuilder(
   684  						ctx,
   685  						cli,
   686  
   687  						docs.Image+":"+viper.GetString(tagFlag),
   688  						viper.GetBool(pullFlag),
   689  						viper.GetString(srcFlag),
   690  						filepath.Join(viper.GetString(dstFlag), cfg.Docs.Path),
   691  						handleID,
   692  						os.Stdout,
   693  						branchID,
   694  						branchName,
   695  						cfg.Go.Main,
   696  						cfg,
   697  						viper.GetBool(overwriteFlag),
   698  					),
   699  				)
   700  			}
   701  		}
   702  
   703  		semaphore := make(chan struct{}, viper.GetInt(concurrencyFlag))
   704  		var wg sync.WaitGroup
   705  		for _, b := range bdrs {
   706  			wg.Add(1)
   707  
   708  			semaphore <- struct{}{}
   709  
   710  			go func(builder builders.Builder) {
   711  				defer func() {
   712  					<-semaphore
   713  
   714  					wg.Done()
   715  				}()
   716  
   717  				if viper.GetBool(ejectFlag) {
   718  					if err := builder.Render(viper.GetString(srcFlag), true); err != nil {
   719  						panic(err)
   720  					}
   721  				} else {
   722  					if err := builder.Build(); err != nil {
   723  						panic(err)
   724  					}
   725  				}
   726  			}(b)
   727  		}
   728  
   729  		wg.Wait()
   730  
   731  		return nil
   732  	},
   733  }
   734  
   735  func init() {
   736  	pwd, err := os.Getwd()
   737  	if err != nil {
   738  		panic(err)
   739  	}
   740  
   741  	buildCmd.PersistentFlags().String(configFlag, "hydrapp.yaml", "Config file to use")
   742  
   743  	buildCmd.PersistentFlags().Bool(pullFlag, false, "Whether to (re-)pull the images or not")
   744  	buildCmd.PersistentFlags().String(tagFlag, "latest", "Image tag to use")
   745  	buildCmd.PersistentFlags().Int(concurrencyFlag, 1, "Maximum amount of concurrent builders to run at once")
   746  	buildCmd.PersistentFlags().Bool(ejectFlag, false, "Write platform-specific config files (AndroidManifest.xml, .spec etc.) to directory specified by --src, then exit (--exclude still applies)")
   747  	buildCmd.PersistentFlags().Bool(overwriteFlag, false, "Overwrite platform-specific config files even if they exist")
   748  
   749  	buildCmd.PersistentFlags().String(srcFlag, pwd, "Source directory (must be absolute path)")
   750  	buildCmd.PersistentFlags().String(dstFlag, filepath.Join(pwd, "out"), "Output directory (must be absolute path)")
   751  
   752  	buildCmd.PersistentFlags().String(excludeFlag, "", "Regex of platforms and architectures not to build for, i.e. (binaries|deb|rpm|flatpak/amd64|msi/386|dmg|docs|tests)")
   753  
   754  	buildCmd.PersistentFlags().String(javaKeystoreFlag, "", "Path to Java/APK keystore (neither path nor content should be not base64-encoded)")
   755  	buildCmd.PersistentFlags().String(javaKeystorePasswordFlag, "", "Java/APK keystore password (base64-encoded)")
   756  	buildCmd.PersistentFlags().String(javaCertificatePasswordFlag, "", " Java/APK certificate password (base64-encoded) (if keystore uses PKCS12, this will be the same as --java-keystore-password)")
   757  
   758  	buildCmd.PersistentFlags().String(pgpKeyFlag, "", "Path to armored PGP private key (neither path nor content should be not base64-encoded)")
   759  	buildCmd.PersistentFlags().String(pgpKeyPasswordFlag, "", "PGP key password (base64-encoded)")
   760  	buildCmd.PersistentFlags().String(pgpKeyIDFlag, "", "PGP key ID (base64-encoded)")
   761  
   762  	buildCmd.PersistentFlags().String(branchIDFlag, "", `Branch ID to build the app as, i.e. main (for an app ID like "myappid.main" and baseURL like "mybaseurl/main") (fetched from Git unless set)`)
   763  	buildCmd.PersistentFlags().String(branchNameFlag, "", `Branch name to build the app as, i.e. Main (for an app name like "myappname (Main)") (fetched from Git unless set)`)
   764  	buildCmd.PersistentFlags().Int64(branchTimestampFlag, 0, `Branch UNIX timestamp to build the app with, i.e. 1715484587 (fetched from Git unless set)`)
   765  
   766  	viper.AutomaticEnv()
   767  
   768  	rootCmd.AddCommand(buildCmd)
   769  }