-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathmadb.go
930 lines (765 loc) · 26.6 KB
/
madb.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The following enables go generate to generate the doc.go file.
//go:generate go run $JIRI_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
// The following generates the embedded_gradle.go file from the madb_init.gradle file.
//go:generate go run scripts/embed_gradle_script.go madb_init.gradle embedded_gradle.go gradleInitScript
// The following generates the version.go file with the version string defined in the MADB_VERSION file.
//go:generate go run scripts/update_version.go
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"v.io/x/lib/cmdline"
"v.io/x/lib/gosh"
"v.io/x/lib/textutil"
)
var (
allDevicesFlag bool
allEmulatorsFlag bool
devicesFlag string
sequentialFlag bool
prefixFlag string
clearCacheFlag bool
moduleFlag string
variantFlag string
buildFlag bool
wd string // working directory
)
func init() {
cmdMadb.Flags.BoolVar(&allDevicesFlag, "d", false, `Restrict the command to only run on real devices.`)
cmdMadb.Flags.BoolVar(&allEmulatorsFlag, "e", false, `Restrict the command to only run on emulators.`)
cmdMadb.Flags.StringVar(&devicesFlag, "n", "", `Comma-separated device serials, qualifiers, device indices (e.g., '@1', '@2'), nicknames (set by 'madb name'), or group names (set by 'madb group'). A device index is specified by an '@' sign followed by the index of the device in the output of 'adb devices' command, starting from 1. Command will be run only on specified devices.`)
cmdMadb.Flags.BoolVar(&sequentialFlag, "seq", false, `Run the command sequentially, instead of running it in parallel.`)
cmdMadb.Flags.StringVar(&prefixFlag, "prefix", "name", `Specify which output prefix to use. You can choose from the following options:
name - Display the nickname of the device. The serial number is used instead if the
nickname is not set for the given device.
serial - Display the serial number of the device.
none - Do not display the output prefix.`)
// Store the current working directory.
var err error
wd, err = os.Getwd()
if err != nil {
panic(err)
}
}
// initializePropertyCacheFlags sets up the flags related to extracting and caching project properties.
func initializePropertyCacheFlags(flags *flag.FlagSet) {
flags.BoolVar(&clearCacheFlag, "clear-cache", false, `Clear the cache and re-extract the variant properties such as the application ID and the main activity name. Only takes effect when no arguments are provided.`)
flags.StringVar(&moduleFlag, "module", "", `Specify which application module to use, when the current directory is the top level Gradle project containing multiple sub-modules. When not specified, the first available application module is used. Only takes effect when no arguments are provided.`)
flags.StringVar(&variantFlag, "variant", "", `Specify which build variant to use. When not specified, the first available build variant is used. Only takes effect when no arguments are provided.`)
}
// initializeBuildFlags sets up the flags related to running Gradle build tasks.
func initializeBuildFlags(flags *flag.FlagSet) {
flags.BoolVar(&buildFlag, "build", true, `Build the target app variant before installing or running the app.`)
}
var cmdMadb = &cmdline.Command{
Children: []*cmdline.Command{
cmdMadbClearData,
cmdMadbExec,
cmdMadbExtern,
cmdMadbGroup,
cmdMadbInstall,
cmdMadbName,
cmdMadbResolve,
cmdMadbShell,
cmdMadbStart,
cmdMadbStop,
cmdMadbUninstall,
cmdMadbUser,
cmdMadbVersion,
},
Name: "madb",
Short: "Multi-device Android Debug Bridge",
Long: `
Multi-device Android Debug Bridge
The madb command wraps Android Debug Bridge (adb) command line tool
and provides various features for controlling multiple Android devices concurrently.
`,
}
func main() {
cmdline.Main(cmdMadb)
}
// Makes sure that adb server is running.
// Intended to be called at the beginning of each subcommand.
func startAdbServer() error {
// TODO(youngseokyoon): search for installed adb tool more rigourously.
if err := exec.Command("adb", "start-server").Run(); err != nil {
return fmt.Errorf("Failed to start adb server. Please make sure that adb is in your PATH: %v", err)
}
return nil
}
type deviceType string
const (
emulator deviceType = "Emulator"
realDevice deviceType = "RealDevice"
)
type device struct {
Serial string
Type deviceType
Qualifiers []string
Nickname string
Index int
UserID string
}
// Returns the display name which is intended to be used as the console output prefix.
// This would be the nickname of the device if there is one; otherwise, the serial number is used.
func (d device) displayName() string {
if d.Nickname != "" {
return d.Nickname
}
return d.Serial
}
// Runs "adb devices -l" command, and parses the result to get all the device serial numbers.
func getDevices(cfg *config) ([]device, error) {
sh := gosh.NewShell(nil)
defer sh.Cleanup()
output := sh.Cmd("adb", "devices", "-l").Stdout()
return parseDevicesOutput(output, cfg)
}
// Parses the output generated from "adb devices -l" command and return the list of device serial numbers
// Devices that are currently offline are excluded from the returned list.
func parseDevicesOutput(output string, cfg *config) ([]device, error) {
lines := strings.Split(output, "\n")
result := []device{}
// Check the first line of the output
if len(lines) <= 0 || strings.TrimSpace(lines[0]) != "List of devices attached" {
return result, fmt.Errorf("The output from 'adb devices -l' command does not look as expected.")
}
// Iterate over all the device serial numbers, starting from the second line.
for i, line := range lines[1:] {
fields := strings.Fields(line)
if len(fields) <= 1 || fields[1] == "offline" {
continue
}
// Fill in the device serial, all the qualifiers, and the device index.
d := device{
Serial: fields[0],
Qualifiers: fields[2:],
Index: i + 1,
}
// Determine whether this device is an emulator or a real device.
if strings.HasPrefix(d.Serial, "emulator") {
d.Type = emulator
} else {
d.Type = realDevice
}
if cfg != nil {
// Determine whether there is a nickname defined for this device,
// so that the console output prefix can display the nickname instead of the serial.
NSMLoop:
for nickname, serial := range cfg.Names {
if d.Serial == serial {
d.Nickname = nickname
break
}
for _, qualifier := range d.Qualifiers {
if qualifier == serial {
d.Nickname = nickname
break NSMLoop
}
}
}
// Determine whether there is a default user ID set by 'madb user'.
if userID, ok := cfg.UserIDs[d.Serial]; ok {
d.UserID = userID
}
}
result = append(result, d)
}
return result, nil
}
// Gets all the devices specified by the device specifier flags.
// Intended to be used by most of the madb sub-commands except for 'madb name'.
func getSpecifiedDevices() ([]device, error) {
configFile, err := getDefaultConfigFilePath()
if err != nil {
return nil, err
}
cfg, err := readConfig(configFile)
if err != nil {
return nil, err
}
devices, err := getDevices(cfg)
if err != nil {
return nil, err
}
tokens := strings.Split(devicesFlag, ",")
filtered, err := filterSpecifiedDevices(devices, cfg, allDevicesFlag, allEmulatorsFlag, tokens)
if err != nil {
return nil, err
}
if len(filtered) == 0 {
return nil, fmt.Errorf("No devices matching the device specifiers.")
}
return filtered, nil
}
type deviceSpec struct {
index int
token string
}
func filterSpecifiedDevices(devices []device, cfg *config, allDevices, allEmulators bool, tokens []string) ([]device, error) {
// If the tokens only contains one empty string, treat it as an empty slice.
if len(tokens) == 1 && tokens[0] == "" {
tokens = []string{}
}
// If no device specifier flags are set, run on all devices and emulators.
if allDevices == false && allEmulators == false && len(tokens) == 0 {
return devices, nil
}
result := make([]device, 0, len(devices))
var specs = []deviceSpec{}
if len(tokens) > 0 {
// Check if the provided specifiers are all valid.
for _, token := range tokens {
if err := isValidDeviceSpecifier(token); err != nil {
return nil, err
}
}
// Expand all the groups and get the device specs.
tokens = expandGroups(tokens, cfg)
specs = getDeviceSpecsFromTokens(tokens, cfg)
}
for _, d := range devices {
if shouldIncludeDevice(d, specs, allDevices, allEmulators) {
result = append(result, d)
}
}
return result, nil
}
// getDeviceSpecsFromTokens takes device specifier tokens and turns them into
// the corresponding deviceSpec structs.
func getDeviceSpecsFromTokens(tokens []string, cfg *config) []deviceSpec {
specs := make([]deviceSpec, 0, len(tokens)*2)
for _, token := range tokens {
if strings.HasPrefix(token, "@") {
index, _ := strconv.Atoi(token[1:])
specs = append(specs, deviceSpec{index, ""})
} else {
specs = append(specs, deviceSpec{0, token})
}
}
return specs
}
func shouldIncludeDevice(d device, specs []deviceSpec, allDevices, allEmulators bool) bool {
if allDevices && d.Type == realDevice {
return true
}
if allEmulators && d.Type == emulator {
return true
}
for _, spec := range specs {
// Ignore empty tokens
if spec.index == 0 && spec.token == "" {
continue
}
if spec.index > 0 {
if d.Index == spec.index {
return true
}
continue
}
if d.Serial == spec.token || d.Nickname == spec.token {
return true
}
for _, qualifier := range d.Qualifiers {
if qualifier == spec.token {
return true
}
}
}
return false
}
// config contains various configuration information for madb.
type config struct {
// Version indicates the version string of madb binary by which this config
// was written to the file, in case it has to be migrated to a newer schema.
Version string
// Names keeps the mapping between device nicknames and their serials.
Names map[string]string
// Groups keeps the device group definitions. A group can contain multiple
// devices, each of which is denoted by its name, serial, or index. A group
// can also include other groups.
Groups map[string][]string
// UserIDs keeps the mapping between device serials and their default user
// IDs.
UserIDs map[string]string
}
func newConfig() *config {
return &config{
Names: make(map[string]string),
Groups: make(map[string][]string),
UserIDs: make(map[string]string),
}
}
// Returns the config dir located at "~/.madb"
func getConfigDir() (string, error) {
home := os.Getenv("HOME")
if home == "" {
return "", fmt.Errorf("Could not find the HOME directory.")
}
configDir := filepath.Join(home, ".madb")
if err := os.MkdirAll(configDir, 0755); err != nil {
return "", err
}
if err := migrateOldConfigFiles(configDir); err != nil {
fmt.Fprintf(os.Stderr, "WARNING: Could not successfully migrate the old config files to the newer format: %v", err)
}
return configDir, nil
}
// migrateOldConfigFiles checks if there are old config files (for madb v1.x) in
// the provided config directory. If there are, it migrates these configs to the
// new format, so that users can preserve their device nicknames and user IDs
// when upgrading madb to a newer version.
// TODO(youngseokyoon): remove this migration code in the future.
func migrateOldConfigFiles(configDir string) error {
// Do not try migrating if the new format "config" file already exists.
configFile := filepath.Join(configDir, "config")
if _, err := os.Stat(configFile); err == nil {
return nil
}
cfg := newConfig()
if err := migrateOldConfig(configDir, "nicknames", &cfg.Names); err != nil {
return err
}
if err := migrateOldConfig(configDir, "users", &cfg.UserIDs); err != nil {
return err
}
return writeConfig(cfg, configFile)
}
// migrateOldConfig reads an old config file, which contains a JSON-encoded map,
// and writes the contents to the given map pointer (data).
func migrateOldConfig(configDir, filename string, data *map[string]string) error {
configFile := filepath.Join(configDir, filename)
if _, err := os.Stat(configFile); os.IsNotExist(err) {
return nil
}
f, err := os.Open(configFile)
if err != nil {
return err
}
defer f.Close()
decoder := json.NewDecoder(f)
if err := decoder.Decode(data); err != nil {
data = new(map[string]string)
return fmt.Errorf("Could not read the old config file %q: %v", filename, err)
}
fmt.Printf("NOTE: Migrating the %q file to the newer format.\n", filename)
// Rename the old config as a backup
if err := os.Rename(configFile, configFile+".bak"); err != nil {
return fmt.Errorf("Could not rename the %q file: %v", filename, err)
}
fmt.Printf("NOTE: The backup file can be found at %q.\n", filepath.Join(configDir, filename+".bak"))
return nil
}
// getDefaultConfigFilePath returns the default location of the config file.
func getDefaultConfigFilePath() (string, error) {
configDir, err := getConfigDir()
if err != nil {
return "", err
}
return filepath.Join(configDir, "config"), nil
}
// readConfig reads the provided file and reconstructs the config struct.
// When the file does not exist, it returns an empty config with the members
// initialized as empty maps.
func readConfig(filename string) (*config, error) {
result := newConfig()
// The file may not exist or be empty when there are no stored data.
if stat, err := os.Stat(filename); os.IsNotExist(err) || (err == nil && stat.Size() == 0) {
return result, nil
}
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
decoder := json.NewDecoder(f)
// Decoding might fail when the file is somehow corrupted, or when the schema is updated.
// In such cases, move on after resetting the cache file instead of exiting the app.
if err := decoder.Decode(result); err != nil {
fmt.Fprintf(os.Stderr, "WARNING: Could not decode the file: %q. Resetting the file.\n", err)
if err := os.Remove(f.Name()); err != nil {
return nil, err
}
result = new(config)
}
if result.Names == nil {
result.Names = make(map[string]string)
}
if result.Groups == nil {
result.Groups = make(map[string][]string)
}
if result.UserIDs == nil {
result.UserIDs = make(map[string]string)
}
return result, nil
}
// writeConfig takes a config and writes it into the provided file name.
func writeConfig(cfg *config, filename string) error {
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
cfg.Version = version
encoder := json.NewEncoder(f)
return encoder.Encode(*cfg)
}
func isNameInUse(name string, cfg *config) bool {
return isDeviceNickname(name, cfg) || isGroupName(name, cfg)
}
func isDeviceNickname(name string, cfg *config) bool {
_, ok := cfg.Names[name]
return ok
}
func isGroupName(name string, cfg *config) bool {
_, ok := cfg.Groups[name]
return ok
}
func isValidSerial(serial string) bool {
r := regexp.MustCompile(`^([A-Za-z0-9:\-\._]+|@\d+)$`)
return r.MatchString(serial)
}
func isValidName(name string) bool {
r := regexp.MustCompile(`^\w+$`)
return r.MatchString(name)
}
// isValidMember takes a member string given as an argument, and returns nil
// when the member string is valid. Otherwise, an error is returned indicating
// the reason why the given member string is not valid.
func isValidDeviceSpecifier(member string) error {
if strings.HasPrefix(member, "@") {
index, err := strconv.Atoi(member[1:])
if err != nil || index <= 0 {
return fmt.Errorf("Invalid device specifier %q. '@' sign must be followed by a numeric device index starting from 1.", member)
}
return nil
} else if !isValidSerial(member) && !isValidName(member) {
return fmt.Errorf("Invalid device specifier %q. Not a valid serial or a nickname.", member)
}
return nil
}
type subCommandRunner struct {
// init is an optional function that does some initial work that should only
// be performed once, before directing the command to all the devices.
// The returned string slice becomes the new set of arguments passed into
// the sub command.
init func(env *cmdline.Env, args []string, properties variantProperties) ([]string, error)
// subCmd defines the behavior of the sub command which will run on all the
// devices in parallel.
subCmd func(env *cmdline.Env, args []string, d device, properties variantProperties) error
// extractProperties indicates whether this subCommand needs the extracted
// project properties.
extractProperties bool
}
var _ cmdline.Runner = (*subCommandRunner)(nil)
// Invokes the sub command on all the devices in parallel.
func (r subCommandRunner) Run(env *cmdline.Env, args []string) error {
prefixFlag = strings.ToLower(prefixFlag)
allowed := []string{"name", "serial", "none"}
if !isStringInSlice(prefixFlag, allowed) {
return fmt.Errorf("The -prefix flag value must be one of %v", strings.Join(allowed, ", "))
}
if err := startAdbServer(); err != nil {
return err
}
devices, err := getSpecifiedDevices()
if err != nil {
return err
}
// Extract the properties if needed.
properties := variantProperties{}
if r.extractProperties && isGradleProject(wd) {
properties, err = getProjectPropertiesUsingDefaultCache()
if err != nil {
return err
}
}
// Run the init function when provided.
if r.init != nil {
newArgs, err := r.init(env, args, properties)
if err != nil {
return err
}
args = newArgs
}
var errs []error
var errDevices []device
if sequentialFlag {
for _, d := range devices {
if err := r.subCmd(env, args, d, properties); err != nil {
errs = append(errs, err)
errDevices = append(errDevices, d)
}
}
} else {
wg := sync.WaitGroup{}
for _, d := range devices {
// Capture the current device value, and run the command in a go-routine.
deviceCopy := d
wg.Add(1)
go func() {
if err := r.subCmd(env, args, deviceCopy, properties); err != nil {
errs = append(errs, err)
errDevices = append(errDevices, deviceCopy)
}
wg.Done()
}()
}
wg.Wait()
}
// Report any errors returned from the go-routines.
if errs != nil {
buffer := bytes.Buffer{}
buffer.WriteString("Error occurred while running the command on the following devices:")
for i := 0; i < len(errs); i++ {
buffer.WriteString("\n[" + errDevices[i].displayName() + "]\t" + errs[i].Error())
}
return fmt.Errorf(buffer.String())
}
return nil
}
func runGoshCommandForDevice(cmd *gosh.Cmd, d device, printUserID bool) error {
return runGoshCommandForDeviceWithWriters(cmd, d, printUserID, os.Stdout, os.Stderr)
}
func runGoshCommandForDeviceWithWriters(cmd *gosh.Cmd, d device, printUserID bool, stdout, stderr io.Writer) error {
prefix := ""
if prefixFlag != "none" {
name := d.Serial
if prefixFlag == "name" {
name = d.displayName()
}
if printUserID && d.UserID != "" {
name = name + ":" + d.UserID
}
prefix = "[" + name + "]\t"
}
prefixedStdout := textutil.PrefixLineWriter(stdout, prefix)
prefixedStderr := textutil.PrefixLineWriter(stderr, prefix)
cmd.AddStdoutWriter(prefixedStdout)
cmd.AddStderrWriter(prefixedStderr)
cmd.Run()
prefixedStdout.Flush()
prefixedStderr.Flush()
return cmd.Shell().Err
}
func initMadbCommand(env *cmdline.Env, args []string, properties variantProperties, flutterPassthrough bool, activityNameRequired bool) ([]string, error) {
var numRequiredArgs int
var requiredArgsStr string
if activityNameRequired {
numRequiredArgs = 2
requiredArgsStr = "two arguments"
} else {
numRequiredArgs = 1
requiredArgsStr = "one argument"
}
// Pass the arguments through if all the required arguments are provided, or if it is a flutter project.
if len(args) == numRequiredArgs || (flutterPassthrough && isFlutterProject(wd)) {
return args, nil
}
if len(args) != 0 {
return nil, fmt.Errorf("You mush provide either zero arguments or exactly %v.", requiredArgsStr)
}
// Try to extract the application ID and the main activity name from the Gradle scripts.
if isGradleProject(wd) {
args = []string{properties.AppID, properties.Activity}[:numRequiredArgs]
}
return args, nil
}
func getProjectPropertiesUsingDefaultCache() (variantProperties, error) {
cacheFile, err := getDefaultCacheFilePath()
if err != nil {
return variantProperties{}, err
}
key := variantKey{wd, moduleFlag, variantFlag}
return getProjectProperties(extractPropertiesFromGradle, key, clearCacheFlag, cacheFile)
}
type variantProperties struct {
ProjectPath string
VariantName string
CleanTask string
AssembleTask string
AppID string
Activity string
AbiFilters []string
VariantOutputs []variantOutput
}
type variantOutput struct {
Name string
OutputFilePath string
VersionCode int
Filters []filter
}
type filter struct {
FilterType string
Identifier string
}
type propertyExtractorFunc func(variantKey) (variantProperties, error)
// getProjectProperties returns the project properties for the given build variant.
// It returns the cached values when the variant is found in the cache file, unless the clearCache
// argument is true. Otherwise, it calls extractPropertiesFromGradle to extract those properties by
// running Gradle scripts.
func getProjectProperties(extractor propertyExtractorFunc, key variantKey, clearCache bool, cacheFile string) (variantProperties, error) {
if clearCache {
clearPropertyCacheEntry(key, cacheFile)
} else {
// See if the current configuration appears in the cache.
cache, err := getPropertyCache(cacheFile)
if err != nil {
return variantProperties{}, err
}
if properties, ok := cache[key]; ok {
fmt.Println("NOTE: Cached IDs are being used. Use '-clear-cache' flag to clear the cache and extract the IDs from Gradle scripts again.")
return properties, nil
}
}
fmt.Println("Running Gradle to extract the application ID and the main activity name...")
properties, err := extractor(key)
if err != nil {
return variantProperties{}, err
}
// Write these properties to the cache.
if err := writePropertyCacheEntry(key, properties, cacheFile); err != nil {
return variantProperties{}, fmt.Errorf("Could not write properties to the cache file: %v", err)
}
return properties, nil
}
func isFlutterProject(dir string) bool {
_, err := os.Stat(filepath.Join(dir, "flutter.yaml"))
return err == nil
}
func isGradleProject(dir string) bool {
_, err := os.Stat(filepath.Join(dir, "build.gradle"))
return err == nil
}
// Looks for the Gradle wrapper script file ("gradlew"), starting from the current directory.
func findGradleWrapper(dir string) (string, error) {
curDir, err := filepath.Abs(dir)
if err != nil {
return "", err
}
for {
wrapperPath := filepath.Join(curDir, "gradlew")
// Return the path of the gradle wrapper script if it is found.
_, err := os.Stat(wrapperPath)
if err == nil {
// Found Gradle wrapper. Return the absolute path.
return wrapperPath, nil
} else if !os.IsNotExist(err) {
// This is an unexpected error and should be returned.
return "", err
}
// Search again in the parent directory.
parentDir := path.Dir(curDir)
if curDir == parentDir || parentDir == "." {
break
}
curDir = parentDir
}
return "", fmt.Errorf("Could not find the Gradle wrapper in dir %q or its parent directories.", dir)
}
func extractPropertiesFromGradle(key variantKey) (variantProperties, error) {
sh := gosh.NewShell(nil)
defer sh.Cleanup()
// Continue on error instead of panicking and check the sh.Err value afterwards.
// Gradle build will finish with exit code other than 0, when it fails.
// In such cases, we want to show users meaningful error messages instead of stacktraces.
sh.PropagateChildOutput = true
sh.ContinueOnError = true
wrapper, err := findGradleWrapper(key.Dir)
if err != nil {
return variantProperties{}, err
}
// Write the init script in a temp file.
initScript := sh.MakeTempFile()
initScript.WriteString(gradleInitScript)
initScript.Sync()
// Create a temporary file in which Gradle can write the results.
outputFile := sh.MakeTempFile()
// Run the gradle wrapper to extract the application ID and the main activity name from the build scripts.
cmdArgs := []string{"--daemon", "-q", "-I", initScript.Name(), "-PmadbOutputFile=" + outputFile.Name()}
// Specify the project directory. If the module name is explicitly set, combine it with the base directory.
cmdArgs = append(cmdArgs, "-p", filepath.Join(key.Dir, key.Module))
// Specify the variant
if key.Variant != "" {
cmdArgs = append(cmdArgs, "-PmadbVariant="+key.Variant)
}
// Specify the tasks
cmdArgs = append(cmdArgs, "madbExtractVariantProperties")
cmd := sh.Cmd(wrapper, cmdArgs...)
cmd.Run()
if err = sh.Err; err != nil {
return variantProperties{}, err
}
// Read what is written in the temporary file.
// The file must be in JSON format.
result := variantProperties{}
decoder := json.NewDecoder(outputFile)
if err = decoder.Decode(&result); err != nil {
return variantProperties{}, fmt.Errorf("Could not extract the application ID and the main activity name: %v", err)
}
return result, nil
}
// expandKeywords takes a command line argument and a device configuration, and returns a new
// argument where the predefined keywords ("{{index}}", "{{name}}", "{{serial}}") are expanded.
func expandKeywords(arg string, d device) string {
exp := regexp.MustCompile(`{{(index|name|serial)}}`)
result := exp.ReplaceAllStringFunc(arg, func(keyword string) string {
switch keyword {
case "{{index}}":
return strconv.Itoa(d.Index)
case "{{name}}":
return d.displayName()
case "{{serial}}":
return d.Serial
default:
return keyword
}
})
return result
}
// isStringInSlice determines whether the given string appears in the slice.
func isStringInSlice(str string, slice []string) bool {
for _, elem := range slice {
if str == elem {
return true
}
}
return false
}
type pathProvider func() (string, error)
// subCommandRunnerWithFilepath is an adapter that turns the madb
// {group|name|user} subcommand functions into cmdline.Runners.
type subCommandRunnerWithFilepath struct {
subCmd func(*cmdline.Env, []string, string) error
pp pathProvider
}
var _ cmdline.Runner = (*subCommandRunnerWithFilepath)(nil)
// Run implements the cmdline.Runner interface by providing the default name
// file path as the third string argument of the underlying run function.
func (f subCommandRunnerWithFilepath) Run(env *cmdline.Env, args []string) error {
p, err := f.pp()
if err != nil {
return err
}
return f.subCmd(env, args, p)
}
// byFirstElement is used for sorting the groups by their names. Used in various
// list commands.
type byFirstElement [][]string
func (a byFirstElement) Len() int { return len(a) }
func (a byFirstElement) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byFirstElement) Less(i, j int) bool { return a[i][0] < a[j][0] }