1// Copyright 2021 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package main
16
17import (
18	"flag"
19	"fmt"
20	"io/ioutil"
21	"os"
22	"path/filepath"
23	"strings"
24
25	"google.golang.org/protobuf/encoding/prototext"
26	"google.golang.org/protobuf/proto"
27
28	"android/soong/compliance/license_metadata_proto"
29	"android/soong/response"
30)
31
32func newMultiString(flags *flag.FlagSet, name, usage string) *multiString {
33	var f multiString
34	flags.Var(&f, name, usage)
35	return &f
36}
37
38type multiString []string
39
40func (ms *multiString) String() string     { return strings.Join(*ms, ", ") }
41func (ms *multiString) Set(s string) error { *ms = append(*ms, s); return nil }
42
43func main() {
44	var expandedArgs []string
45	for _, arg := range os.Args[1:] {
46		if strings.HasPrefix(arg, "@") {
47			f, err := os.Open(strings.TrimPrefix(arg, "@"))
48			if err != nil {
49				fmt.Fprintln(os.Stderr, err.Error())
50				os.Exit(1)
51			}
52
53			respArgs, err := response.ReadRspFile(f)
54			f.Close()
55			if err != nil {
56				fmt.Fprintln(os.Stderr, err.Error())
57				os.Exit(1)
58			}
59			expandedArgs = append(expandedArgs, respArgs...)
60		} else {
61			expandedArgs = append(expandedArgs, arg)
62		}
63	}
64
65	flags := flag.NewFlagSet("flags", flag.ExitOnError)
66
67	packageName := flags.String("p", "", "license package name")
68	moduleType := newMultiString(flags, "mt", "module type")
69	moduleName := flags.String("mn", "", "module name")
70	kinds := newMultiString(flags, "k", "license kinds")
71	moduleClass := newMultiString(flags, "mc", "module class")
72	conditions := newMultiString(flags, "c", "license conditions")
73	notices := newMultiString(flags, "n", "license notice file")
74	deps := newMultiString(flags, "d", "license metadata file dependency")
75	sources := newMultiString(flags, "s", "source (input) dependency")
76	built := newMultiString(flags, "t", "built targets")
77	installed := newMultiString(flags, "i", "installed targets")
78	roots := newMultiString(flags, "r", "root directory of project")
79	installedMap := newMultiString(flags, "m", "map dependent targets to their installed names")
80	isContainer := flags.Bool("is_container", false, "preserved dependent target name when given")
81	outFile := flags.String("o", "", "output file")
82
83	flags.Parse(expandedArgs)
84
85	metadata := license_metadata_proto.LicenseMetadata{}
86	metadata.PackageName = proto.String(*packageName)
87	metadata.ModuleName = proto.String(*moduleName)
88	metadata.ModuleTypes = *moduleType
89	metadata.ModuleClasses = *moduleClass
90	metadata.IsContainer = proto.Bool(*isContainer)
91	metadata.Projects = findGitRoots(*roots)
92	metadata.LicenseKinds = *kinds
93	metadata.LicenseConditions = *conditions
94	metadata.LicenseTexts = *notices
95	metadata.Built = *built
96	metadata.Installed = *installed
97	metadata.InstallMap = convertInstalledMap(*installedMap)
98	metadata.Sources = *sources
99	metadata.Deps = convertDependencies(*deps)
100
101	err := writeMetadata(*outFile, &metadata)
102	if err != nil {
103		fmt.Fprintf(os.Stderr, "error: %s\n", err.Error())
104		os.Exit(2)
105	}
106}
107
108func findGitRoots(dirs []string) []string {
109	ret := make([]string, len(dirs))
110	for i, dir := range dirs {
111		ret[i] = findGitRoot(dir)
112	}
113	return ret
114}
115
116// findGitRoot finds the directory at or above dir that contains a ".git" directory.  This isn't
117// guaranteed to exist, for example during remote execution, when sandboxed, when building from
118// infrastructure that doesn't use git, or when the .git directory has been removed to save space,
119// but it should be good enough for local builds.  If no .git directory is found the original value
120// is returned.
121func findGitRoot(dir string) string {
122	orig := dir
123	for dir != "" && dir != "." && dir != "/" {
124		_, err := os.Stat(filepath.Join(dir, ".git"))
125		if err == nil {
126			// Found dir/.git, return dir.
127			return dir
128		} else if !os.IsNotExist(err) {
129			// Error finding .git, return original input.
130			return orig
131		}
132		dir, _ = filepath.Split(dir)
133		dir = strings.TrimSuffix(dir, "/")
134	}
135	return orig
136}
137
138// convertInstalledMap converts a list of colon-separated from:to pairs into InstallMap proto
139// messages.
140func convertInstalledMap(installMaps []string) []*license_metadata_proto.InstallMap {
141	var ret []*license_metadata_proto.InstallMap
142
143	for _, installMap := range installMaps {
144		components := strings.Split(installMap, ":")
145		if len(components) != 2 {
146			panic(fmt.Errorf("install map entry %q contains %d colons, expected 1", installMap, len(components)-1))
147		}
148		ret = append(ret, &license_metadata_proto.InstallMap{
149			FromPath:      proto.String(components[0]),
150			ContainerPath: proto.String(components[1]),
151		})
152	}
153
154	return ret
155}
156
157// convertDependencies converts a colon-separated tuple of dependency:annotation:annotation...
158// into AnnotatedDependency proto messages.
159func convertDependencies(deps []string) []*license_metadata_proto.AnnotatedDependency {
160	var ret []*license_metadata_proto.AnnotatedDependency
161
162	for _, d := range deps {
163		components := strings.Split(d, ":")
164		dep := components[0]
165		components = components[1:]
166		ad := &license_metadata_proto.AnnotatedDependency{
167			File:        proto.String(dep),
168			Annotations: make([]string, 0, len(components)),
169		}
170		for _, ann := range components {
171			if len(ann) == 0 {
172				continue
173			}
174			ad.Annotations = append(ad.Annotations, ann)
175		}
176		ret = append(ret, ad)
177	}
178
179	return ret
180}
181
182func writeMetadata(file string, metadata *license_metadata_proto.LicenseMetadata) error {
183	buf, err := prototext.MarshalOptions{Multiline: true}.Marshal(metadata)
184	if err != nil {
185		return fmt.Errorf("error marshalling textproto: %w", err)
186	}
187
188	if file != "" {
189		err = ioutil.WriteFile(file, buf, 0666)
190		if err != nil {
191			return fmt.Errorf("error writing textproto %q: %w", file, err)
192		}
193	} else {
194		_, _ = os.Stdout.Write(buf)
195	}
196
197	return nil
198}
199