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 }