[JSON] Write JSON file for packages

This patch add a new feature to jiri to generate JSON file for packages.
If there is any internal package and jiri fetched these packages
successfully, jiri will write "internal_access = true" in generated JSON
file. If jiri failed to fetch internal packages, "internal_access =
false" will be written. If there is no internal packages, jiri will not
generate the JSON file. This patch also add a feature to jiri to set up
default paths to packages which do not explicitly define one in
manifest.

Test: CQ
Change-Id: I8d3ded3e8afdcd5aa229860869ba5bd8a6a0f63f
diff --git a/cmd/jiri/init.go b/cmd/jiri/init.go
index 13d56c9..1bc5e7e 100644
--- a/cmd/jiri/init.go
+++ b/cmd/jiri/init.go
@@ -44,6 +44,7 @@
 	keepGitHooks          string
 	enableLockfileFlag    string
 	lockfileNameFlag      string
+	prebuiltJSON          string
 )
 
 func init() {
@@ -56,6 +57,7 @@
 	cmdInit.Flags.StringVar(&keepGitHooks, "keep-git-hooks", "", "Whether to keep current git hooks in '.git/hooks' when doing 'jiri update'. Takes true/false.")
 	cmdInit.Flags.StringVar(&enableLockfileFlag, "enable-lockfile", "", "Enable lockfile enforcement")
 	cmdInit.Flags.StringVar(&lockfileNameFlag, "lockfile-name", "", "Set up filename of lockfile")
+	cmdInit.Flags.StringVar(&prebuiltJSON, "prebuilt-json", "", "Set up filename for prebuilt json file")
 }
 
 func runInit(env *cmdline.Env, args []string) error {
@@ -151,6 +153,10 @@
 		config.LockfileName = lockfileNameFlag
 	}
 
+	if prebuiltJSON != "" {
+		config.PrebuiltJSON = prebuiltJSON
+	}
+
 	if enableLockfileFlag != "" {
 		if val, err := strconv.ParseBool(enableLockfileFlag); err != nil {
 			return fmt.Errorf("'enableLockfileFlag' flag should be true or false")
diff --git a/project/manifest.go b/project/manifest.go
index 5c66729..0918ab1 100644
--- a/project/manifest.go
+++ b/project/manifest.go
@@ -7,6 +7,7 @@
 import (
 	"bytes"
 	"context"
+	"encoding/json"
 	"encoding/xml"
 	"errors"
 	"fmt"
@@ -16,6 +17,7 @@
 	"net/url"
 	"os"
 	"os/exec"
+	"path"
 	"path/filepath"
 	"sort"
 	"strings"
@@ -441,6 +443,35 @@
 	return PackageKey(p.Path + KeySeparator + p.Name)
 }
 
+// FilterACL returns a new Packages map without any inaccessible packages.
+func (p *Packages) FilterACL(jirix *jiri.X) (Packages, bool, error) {
+	// Perform ACL checks on internal projects
+	pkgACLMap := make(map[string]bool)
+	pkgVersionMap := make(map[string]string)
+	hasInternal := false
+	for _, pkg := range *p {
+		pkg.Name = strings.TrimRight(pkg.Name, "/")
+		if pkg.Internal {
+			hasInternal = true
+			pkgACLMap[pkg.Name] = false
+			pkgVersionMap[pkg.Name] = pkg.Version
+		}
+	}
+	if len(pkgACLMap) != 0 {
+		if err := cipd.CheckPackageACL(jirix, pkgACLMap, pkgVersionMap); err != nil {
+			return nil, false, err
+		}
+	}
+	retPkgs := make(Packages)
+	for _, pkg := range *p {
+		if val, ok := pkgACLMap[pkg.Name]; ok && !val {
+			continue
+		}
+		retPkgs[pkg.Key()] = pkg
+	}
+	return retPkgs, hasInternal, nil
+}
+
 type PackageInstance struct {
 	Name    string   `xml:"name,attr"`
 	ID      string   `xml:"id,attr"`
@@ -461,6 +492,35 @@
 	return nil
 }
 
+// GetPath returns the relative path that Package p should be
+// downloaded to.
+func (p *Package) GetPath() (string, error) {
+	if p.Path == "" {
+		cipdPath := p.Name
+		// Replace template with current platform information.
+		// If failed, skip filling in default path.
+		if cipd.MustExpand(cipdPath) {
+			expanded, err := cipd.Expand(cipdPath, []cipd.Platform{cipd.CipdPlatform})
+			if err != nil {
+				return "", err
+			}
+			if len(expanded) > 0 {
+				cipdPath = expanded[0]
+			}
+		}
+		if !cipd.MustExpand(cipdPath) {
+			base := path.Base(cipdPath)
+			if _, err := cipd.NewPlatform(base); err == nil {
+				// base is the name for a platform
+				base = filepath.Join(path.Base(path.Dir(cipdPath)), base)
+			}
+			return filepath.Join("prebuilt", base), nil
+		}
+		return "prebuilt", nil
+	}
+	return p.Path, nil
+}
+
 // GetPlatforms returns the platforms information of
 // this Package struct.
 func (p *Package) GetPlatforms() ([]cipd.Platform, error) {
@@ -605,6 +665,11 @@
 	jirix.TimerPush("resove instance id for cipd packages")
 	defer jirix.TimerPop()
 
+	pkgs, _, err := pkgs.FilterACL(jirix)
+	if err != nil {
+		return nil, err
+	}
+
 	ensureFilePath, err := generateEnsureFile(jirix, pkgs, false)
 	if err != nil {
 		return nil, err
@@ -642,7 +707,12 @@
 	jirix.TimerPush("fetch cipd packages")
 	defer jirix.TimerPop()
 
-	ensureFilePath, err := generateEnsureFile(jirix, pkgs, !jirix.LockfileEnabled || jirix.UsingSnapshot)
+	pkgsWAccess, hasInternalPkgs, err := pkgs.FilterACL(jirix)
+	if err != nil {
+		return err
+	}
+
+	ensureFilePath, err := generateEnsureFile(jirix, pkgsWAccess, !jirix.LockfileEnabled || jirix.UsingSnapshot)
 	if err != nil {
 		return err
 	}
@@ -660,9 +730,42 @@
 		return err
 	}
 
+	if hasInternalPkgs {
+		if err := writePackageJSON(jirix, len(pkgs) == len(pkgsWAccess)); err != nil {
+			return err
+		}
+	}
+
+	if len(pkgs) > len(pkgsWAccess) {
+		cipdLoggedIn, err := cipd.CheckLoggedIn(jirix)
+		if err != nil {
+			return err
+		}
+		if !cipdLoggedIn {
+			jirix.Logger.Warningf("Some packages are skipped by cipd due to lack of access, you might want to run \"cipd auth-login\" and try again")
+		}
+	}
 	return nil
 }
 
+// GenerateJSON generates a json file which contains fetched
+// packages.
+func writePackageJSON(jirix *jiri.X, access bool) error {
+	var internalAccess struct {
+		Access bool `json:"internal_access"`
+	}
+	internalAccess.Access = access
+	jsonData, err := json.MarshalIndent(&internalAccess, "", "    ")
+	if err != nil {
+		return err
+	}
+	if jirix.PrebuiltJSON == "" {
+		// Skip json file creation if PrebuiltJSON is not set.
+		return nil
+	}
+	return ioutil.WriteFile(filepath.Join(jirix.RootMetaDir(), jirix.PrebuiltJSON), jsonData, 0644)
+}
+
 func generateEnsureFile(jirix *jiri.X, pkgs Packages, ignoreCryptoCheck bool) (string, error) {
 	ensureFile, err := ioutil.TempFile("", "jiri*.ensure")
 	if err != nil {
@@ -703,28 +806,8 @@
 	ensureFileBuf.WriteString("$ParanoidMode CheckPresence\n")
 	ensureFileBuf.WriteString("\n")
 
-	// Perform ACL checks on internal projects
-	pkgACLMap := make(map[string]bool)
-	pkgVersionMap := make(map[string]string)
 	for _, pkg := range pkgs {
-		pkg.Name = strings.TrimRight(pkg.Name, "/")
-		if pkg.Internal {
-			pkgACLMap[pkg.Name] = false
-			pkgVersionMap[pkg.Name] = pkg.Version
-		}
-	}
-	if len(pkgACLMap) != 0 {
-		if err := cipd.CheckPackageACL(jirix, pkgACLMap, pkgVersionMap); err != nil {
-			return "", err
-		}
-	}
 
-	hasSkippedPkgs := false
-	for _, pkg := range pkgs {
-		if val, ok := pkgACLMap[pkg.Name]; ok && !val {
-			hasSkippedPkgs = true
-			continue
-		}
 		cipdDecl, err := pkg.cipdDecl(jirix.UsingSnapshot)
 		if err != nil {
 			return "", err
@@ -740,22 +823,17 @@
 	if err := ensureFile.Sync(); err != nil {
 		return "", err
 	}
-	if hasSkippedPkgs {
-		cipdLoggedIn, err := cipd.CheckLoggedIn(jirix)
-		if err != nil {
-			return "", err
-		}
-		if !cipdLoggedIn {
-			jirix.Logger.Warningf("Some packages are skipped by cipd due to lack of access, you might want to run \"cipd auth-login\" and try again")
-		}
-	}
+
 	return ensureFilePath, nil
 }
 
 func (p *Package) cipdDecl(usingSnapshot bool) (string, error) {
 	var buf bytes.Buffer
 	// Write "@Subdir" line to cipd declaration
-	subdir := p.Path
+	subdir, err := p.GetPath()
+	if err != nil {
+		return "", err
+	}
 	tmpl, err := template.New("pack").Parse(subdir)
 	if err != nil {
 		return "", fmt.Errorf("parsing package path %q failed", subdir)
diff --git a/project/project_test.go b/project/project_test.go
index 09ffb6e..0d7b9bf 100644
--- a/project/project_test.go
+++ b/project/project_test.go
@@ -19,6 +19,7 @@
 	"testing"
 
 	"fuchsia.googlesource.com/jiri"
+	"fuchsia.googlesource.com/jiri/cipd"
 	"fuchsia.googlesource.com/jiri/gitutil"
 	"fuchsia.googlesource.com/jiri/jiritest"
 	"fuchsia.googlesource.com/jiri/project"
@@ -2265,3 +2266,31 @@
 	}
 
 }
+
+func TestGetPath(t *testing.T) {
+	testPkgs := []project.Package{
+		project.Package{Name: "test0", Version: "version", Path: "A/test0"},
+		project.Package{Name: "test1/${platform}", Version: "version", Path: ""},
+		project.Package{Name: "test2/${os}-${arch}", Version: "version", Path: ""},
+		project.Package{Name: "test3/${platform=linux-armv6l}", Version: "version", Path: ""},
+	}
+	testResults := []string{
+		"A/test0",
+		"prebuilt/test1/",
+		"prebuilt/test2/",
+		"prebuilt",
+	}
+
+	for i, v := range testPkgs {
+		defaultPath, err := v.GetPath()
+		if err != nil {
+			t.Errorf("TestGetPath failed due to error: %v", err)
+		}
+		if strings.HasSuffix(testResults[i], "/") {
+			testResults[i] += cipd.CipdPlatform.String()
+		}
+		if testResults[i] != defaultPath {
+			t.Errorf("expecting %q, got %q", testResults[i], defaultPath)
+		}
+	}
+}
diff --git a/x.go b/x.go
index 7c98262..dadd7a7 100644
--- a/x.go
+++ b/x.go
@@ -51,6 +51,7 @@
 	SsoCookiePath     string `xml:"SsoCookiePath,omitempty"`
 	LockfileEnabled   bool   `xml:"lockfile>enabled,omitempty"`
 	LockfileName      string `xml:"lockfile>name,omitempty"`
+	PrebuiltJSON      string `xml:"prebuilt>JSON,omitempty"`
 	AnalyticsOptIn    string `xml:"analytics>optin,omitempty"`
 	AnalyticsUserId   string `xml:"analytics>userId,omitempty"`
 	// version user has opted-in to
@@ -105,6 +106,7 @@
 	LockfileEnabled     bool
 	LockfileName        string
 	SsoCookiePath       string
+	PrebuiltJSON        string
 	UsingSnapshot       bool
 	IgnoreLockConflicts bool
 	Color               color.Color
@@ -237,9 +239,13 @@
 		x.SsoCookiePath = x.config.SsoCookiePath
 		x.LockfileEnabled = x.config.LockfileEnabled
 		x.LockfileName = x.config.LockfileName
+		x.PrebuiltJSON = x.config.PrebuiltJSON
 		if x.LockfileName == "" {
 			x.LockfileName = "jiri.lock"
 		}
+		if x.PrebuiltJSON == "" {
+			x.PrebuiltJSON = "prebuilt.json"
+		}
 	}
 	x.Cache, err = findCache(root, x.config)
 	if x.config != nil {