@@ -14,6 +14,7 @@ import (
1414 pages "lampa/internal/templates/html"
1515 "lampa/internal/utils"
1616 "log"
17+ "net/http"
1718 "os"
1819 "os/exec"
1920 "path"
@@ -33,6 +34,9 @@ import (
3334 . "lampa/internal/globals"
3435)
3536
37+ const BundletoolUrl = "https://github.com/google/bundletool/releases/download/1.18.1/bundletool-all-1.18.1.jar"
38+ const BundletoolHash = "e105bfd112a86986bb869d94b831c0e1e571a314"
39+
3640const (
3741 EnvAndroidSdkRoot = "ANDROID_SDK_ROOT"
3842 EnvBundletoolJar = "BUNDLETOOL_JAR"
@@ -114,7 +118,13 @@ func parseExecArgs(c *cli.Command) ExecArgs {
114118 args .GradlewPath = path .Join (args .ProjectDir , "gradlew" )
115119
116120 args .AndroidSdkPath = utils .TryResolveFsPath (os .Getenv (EnvAndroidSdkRoot ))
117- args .BundletoolPath = utils .TryResolveFsPath (os .Getenv (EnvBundletoolJar ))
121+
122+ bundleToolEnv := strings .TrimSpace (os .Getenv (EnvBundletoolJar ))
123+ if bundleToolEnv == "" {
124+ args .NeedToDownloadBundletool = true
125+ } else {
126+ args .BundletoolPath = utils .TryResolveFsPath (bundleToolEnv )
127+ }
118128
119129 return args
120130}
@@ -178,13 +188,16 @@ func validateExecArgs(args *ExecArgs) error {
178188
179189 // Bundletool
180190 if args .BundletoolPath == "" {
181- return fmt .Errorf ("%s environment variable is not set" , EnvBundletoolJar )
182- }
183- if ! utils .FileExists (args .BundletoolPath ) {
184- return fmt .Errorf ("bundletool jar file `%s` does not exist" , args .BundletoolPath )
185- }
186- if utils .IsDir (args .BundletoolPath ) {
187- return fmt .Errorf ("bundletool jar file `%s` is a directory" , args .BundletoolPath )
191+ if ! args .NeedToDownloadBundletool {
192+ return fmt .Errorf ("%s environment variable is not set" , EnvBundletoolJar )
193+ }
194+ } else {
195+ if ! utils .FileExists (args .BundletoolPath ) {
196+ return fmt .Errorf ("bundletool jar file `%s` does not exist" , args .BundletoolPath )
197+ }
198+ if utils .IsDir (args .BundletoolPath ) {
199+ return fmt .Errorf ("bundletool jar file `%s` is a directory" , args .BundletoolPath )
200+ }
188201 }
189202
190203 // Aapt
@@ -244,6 +257,8 @@ type ExecArgs struct {
244257 AndroidSdkPath string
245258 AaptPath string
246259 GradlewPath string
260+
261+ NeedToDownloadBundletool bool
247262}
248263
249264func CmdActionCollect (ctx context.Context , cmd * cli.Command ) error {
@@ -282,11 +297,22 @@ func execute(args ExecArgs) error {
282297 }
283298 }
284299 }
300+ if args .NeedToDownloadBundletool {
301+ hasWarningSection = true
302+ out .PrintlnWarn ("%s is not set, so it will be downloaded automatically." , EnvBundletoolJar )
303+ }
285304 if hasWarningSection {
286305 fmt .Println ()
287306 }
288307
289- _ , err := DynamicSpinner (SpinnerArgs {
308+ var err error
309+
310+ err = StepBundletool (& args )
311+ if err != nil {
312+ return err
313+ }
314+
315+ _ , err = DynamicSpinner (SpinnerArgs {
290316 Msg : "Building..." ,
291317 MsgAfterSuccess : "Building: Done." ,
292318 MsgAfterFail : "Building: Failed." ,
@@ -368,6 +394,113 @@ func execute(args ExecArgs) error {
368394 return nil
369395}
370396
397+ func StepBundletool (args * ExecArgs ) error {
398+ if ! args .NeedToDownloadBundletool {
399+ return nil
400+ }
401+
402+ _ , err := DynamicSpinner (
403+ SpinnerArgs {
404+ Msg : "Downloading bundletool..." ,
405+ MsgAfterSuccess : "Downloading bundletool: Done." ,
406+ MsgAfterFail : "Downloading bundletool: Failed." ,
407+ },
408+ func () (any , error ) {
409+ return nil , stepBundletoolInternal (args )
410+ },
411+ )
412+
413+ return err
414+ }
415+
416+ func stepBundletoolInternal (args * ExecArgs ) error {
417+ // Download bundletool-all-1.18.1.jar to ./.lampa/cache
418+ cacheDir := filepath .Join (args .ProjectDir , ".lampa" , "cache" )
419+ bundletoolFileName := filepath .Base (BundletoolUrl )
420+ bundletoolPath := filepath .Join (cacheDir , bundletoolFileName )
421+
422+ // Ensure cache directory exists
423+ err := os .MkdirAll (cacheDir , 0o755 )
424+ if err != nil {
425+ return fmt .Errorf ("failed to create cache directory for bundletool: %w" , err )
426+ }
427+
428+ // Download if not exists
429+ if ! utils .FileExists (bundletoolPath ) {
430+ // fmt.Printf("Downloading bundletool from %s...\n", BundletoolUrl)
431+ outFile , err := os .Create (bundletoolPath )
432+ if err != nil {
433+ return fmt .Errorf ("failed to create bundletool file: %w" , err )
434+ }
435+ defer outFile .Close ()
436+
437+ client := & http.Client {}
438+ client .CheckRedirect = func (req * http.Request , via []* http.Request ) error {
439+ // Allow up to 10 redirects
440+ if len (via ) >= 10 {
441+ return fmt .Errorf ("stopped after 10 redirects" )
442+ }
443+ return nil
444+ }
445+ req , err := http .NewRequestWithContext (context .Background (), "GET" , BundletoolUrl , nil )
446+ if err != nil {
447+ return fmt .Errorf ("failed to create HTTP request for bundletool: %w" , err )
448+ }
449+ resp , err := client .Do (req )
450+ if err != nil {
451+ return fmt .Errorf ("failed to download bundletool: %w" , err )
452+ }
453+ defer resp .Body .Close ()
454+
455+ if resp .StatusCode != 200 {
456+ return fmt .Errorf ("failed to download bundletool: HTTP %d" , resp .StatusCode )
457+ }
458+
459+ _ , err = io .Copy (outFile , resp .Body )
460+ if err != nil {
461+ return fmt .Errorf ("failed to save bundletool: %w" , err )
462+ }
463+ }
464+
465+ // Verify checksum of downloaded file
466+ expectedChecksum := BundletoolHash
467+ file , err := os .Open (bundletoolPath )
468+ if err != nil {
469+ return fmt .Errorf ("failed to open bundletool file for checksum: %w" , err )
470+ }
471+ defer file .Close ()
472+ hasher := sha1 .New ()
473+ if _ , err := io .Copy (hasher , file ); err != nil {
474+ return fmt .Errorf ("failed to compute checksum of bundletool: %w" , err )
475+ }
476+ actualChecksum := fmt .Sprintf ("%x" , hasher .Sum (nil ))
477+ if actualChecksum != expectedChecksum {
478+ return fmt .Errorf ("bundletool checksum mismatch: expected %s, got %s" , expectedChecksum , actualChecksum )
479+ }
480+
481+ args .BundletoolPath = utils .TryResolveFsPath (bundletoolPath )
482+
483+ // Ensure .lampa/gitignore exists and ignores all content
484+ gitignorePath := filepath .Join (args .ProjectDir , ".lampa" , ".gitignore" )
485+ if ! utils .FileExists (gitignorePath ) {
486+ err := os .MkdirAll (filepath .Dir (gitignorePath ), 0o755 )
487+ if err != nil {
488+ return fmt .Errorf ("failed to create .lampa directory for gitignore: %w" , err )
489+ }
490+ f , err := os .Create (gitignorePath )
491+ if err != nil {
492+ return fmt .Errorf ("failed to create .lampa/gitignore: %w" , err )
493+ }
494+ defer f .Close ()
495+ _ , err = f .WriteString ("*\n " )
496+ if err != nil {
497+ return fmt .Errorf ("failed to write to .lampa/gitignore: %w" , err )
498+ }
499+ }
500+
501+ return nil
502+ }
503+
371504func StepReport (args ExecArgs ) error {
372505 pathToAab , err := findAabFile (args )
373506 if err != nil {
0 commit comments