1// Copyright 2020 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 rust
16
17import (
18	"encoding/json"
19	"fmt"
20	"path"
21
22	"android/soong/android"
23)
24
25// This singleton collects Rust crate definitions and generates a JSON file
26// (${OUT_DIR}/soong/rust-project.json) which can be use by external tools,
27// such as rust-analyzer. It does so when either make, mm, mma, mmm or mmma is
28// called.  This singleton is enabled only if SOONG_GEN_RUST_PROJECT is set.
29// For example,
30//
31//   $ SOONG_GEN_RUST_PROJECT=1 m nothing
32
33const (
34	// Environment variables used to control the behavior of this singleton.
35	envVariableCollectRustDeps = "SOONG_GEN_RUST_PROJECT"
36	rustProjectJsonFileName    = "rust-project.json"
37)
38
39// The format of rust-project.json is not yet finalized. A current description is available at:
40// https://github.com/rust-analyzer/rust-analyzer/blob/master/docs/user/manual.adoc#non-cargo-based-projects
41type rustProjectDep struct {
42	// The Crate attribute is the index of the dependency in the Crates array in rustProjectJson.
43	Crate int    `json:"crate"`
44	Name  string `json:"name"`
45}
46
47type rustProjectCrate struct {
48	DisplayName string            `json:"display_name"`
49	RootModule  string            `json:"root_module"`
50	Edition     string            `json:"edition,omitempty"`
51	Deps        []rustProjectDep  `json:"deps"`
52	Cfg         []string          `json:"cfg"`
53	Env         map[string]string `json:"env"`
54}
55
56type rustProjectJson struct {
57	Roots  []string           `json:"roots"`
58	Crates []rustProjectCrate `json:"crates"`
59}
60
61// crateInfo is used during the processing to keep track of the known crates.
62type crateInfo struct {
63	Idx  int            // Index of the crate in rustProjectJson.Crates slice.
64	Deps map[string]int // The keys are the module names and not the crate names.
65}
66
67type projectGeneratorSingleton struct {
68	project     rustProjectJson
69	knownCrates map[string]crateInfo // Keys are module names.
70}
71
72func rustProjectGeneratorSingleton() android.Singleton {
73	return &projectGeneratorSingleton{}
74}
75
76func init() {
77	android.RegisterSingletonType("rust_project_generator", rustProjectGeneratorSingleton)
78}
79
80// sourceProviderVariantSource returns the path to the source file if this
81// module variant should be used as a priority.
82//
83// SourceProvider modules may have multiple variants considered as source
84// (e.g., x86_64 and armv8). For a module available on device, use the source
85// generated for the target. For a host-only module, use the source generated
86// for the host.
87func sourceProviderVariantSource(ctx android.SingletonContext, rModule *Module) (string, bool) {
88	rustLib, ok := rModule.compiler.(*libraryDecorator)
89	if !ok {
90		return "", false
91	}
92	if rustLib.source() {
93		switch rModule.hod {
94		case android.HostSupported, android.HostSupportedNoCross:
95			if rModule.Target().String() == ctx.Config().BuildOSTarget.String() {
96				src := rustLib.sourceProvider.Srcs()[0]
97				return src.String(), true
98			}
99		default:
100			if rModule.Target().String() == ctx.Config().AndroidFirstDeviceTarget.String() {
101				src := rustLib.sourceProvider.Srcs()[0]
102				return src.String(), true
103			}
104		}
105	}
106	return "", false
107}
108
109// sourceProviderSource finds the main source file of a source-provider crate.
110func sourceProviderSource(ctx android.SingletonContext, rModule *Module) (string, bool) {
111	rustLib, ok := rModule.compiler.(*libraryDecorator)
112	if !ok {
113		return "", false
114	}
115	if rustLib.source() {
116		// This is a source-variant, check if we are the right variant
117		// depending on the module configuration.
118		if src, ok := sourceProviderVariantSource(ctx, rModule); ok {
119			return src, true
120		}
121	}
122	foundSource := false
123	sourceSrc := ""
124	// Find the variant with the source and return its.
125	ctx.VisitAllModuleVariants(rModule, func(variant android.Module) {
126		if foundSource {
127			return
128		}
129		// All variants of a source provider library are libraries.
130		rVariant, _ := variant.(*Module)
131		variantLib, _ := rVariant.compiler.(*libraryDecorator)
132		if variantLib.source() {
133			sourceSrc, ok = sourceProviderVariantSource(ctx, rVariant)
134			if ok {
135				foundSource = true
136			}
137		}
138	})
139	if !foundSource {
140		ctx.Errorf("No valid source for source provider found: %v\n", rModule)
141	}
142	return sourceSrc, foundSource
143}
144
145// crateSource finds the main source file (.rs) for a crate.
146func crateSource(ctx android.SingletonContext, rModule *Module, comp *baseCompiler) (string, bool) {
147	// Basic libraries, executables and tests.
148	srcs := comp.Properties.Srcs
149	if len(srcs) != 0 {
150		return path.Join(ctx.ModuleDir(rModule), srcs[0]), true
151	}
152	// SourceProvider libraries.
153	if rModule.sourceProvider != nil {
154		return sourceProviderSource(ctx, rModule)
155	}
156	return "", false
157}
158
159// mergeDependencies visits all the dependencies for module and updates crate and deps
160// with any new dependency.
161func (singleton *projectGeneratorSingleton) mergeDependencies(ctx android.SingletonContext,
162	module *Module, crate *rustProjectCrate, deps map[string]int) {
163
164	ctx.VisitDirectDeps(module, func(child android.Module) {
165		// Skip intra-module dependencies (i.e., generated-source library depending on the source variant).
166		if module.Name() == child.Name() {
167			return
168		}
169		// Skip unsupported modules.
170		rChild, compChild, ok := isModuleSupported(ctx, child)
171		if !ok {
172			return
173		}
174		// For unknown dependency, add it first.
175		var childId int
176		cInfo, known := singleton.knownCrates[rChild.Name()]
177		if !known {
178			childId, ok = singleton.addCrate(ctx, rChild, compChild)
179			if !ok {
180				return
181			}
182		} else {
183			childId = cInfo.Idx
184		}
185		// Is this dependency known already?
186		if _, ok = deps[child.Name()]; ok {
187			return
188		}
189		crate.Deps = append(crate.Deps, rustProjectDep{Crate: childId, Name: rChild.CrateName()})
190		deps[child.Name()] = childId
191	})
192}
193
194// isModuleSupported returns the RustModule and baseCompiler if the module
195// should be considered for inclusion in rust-project.json.
196func isModuleSupported(ctx android.SingletonContext, module android.Module) (*Module, *baseCompiler, bool) {
197	rModule, ok := module.(*Module)
198	if !ok {
199		return nil, nil, false
200	}
201	if rModule.compiler == nil {
202		return nil, nil, false
203	}
204	var comp *baseCompiler
205	switch c := rModule.compiler.(type) {
206	case *libraryDecorator:
207		comp = c.baseCompiler
208	case *binaryDecorator:
209		comp = c.baseCompiler
210	case *testDecorator:
211		comp = c.binaryDecorator.baseCompiler
212	default:
213		return nil, nil, false
214	}
215	return rModule, comp, true
216}
217
218// addCrate adds a crate to singleton.project.Crates ensuring that required
219// dependencies are also added. It returns the index of the new crate in
220// singleton.project.Crates
221func (singleton *projectGeneratorSingleton) addCrate(ctx android.SingletonContext, rModule *Module, comp *baseCompiler) (int, bool) {
222	rootModule, ok := crateSource(ctx, rModule, comp)
223	if !ok {
224		ctx.Errorf("Unable to find source for valid module: %v", rModule)
225		return 0, false
226	}
227
228	crate := rustProjectCrate{
229		DisplayName: rModule.Name(),
230		RootModule:  rootModule,
231		Edition:     comp.edition(),
232		Deps:        make([]rustProjectDep, 0),
233		Cfg:         make([]string, 0),
234		Env:         make(map[string]string),
235	}
236
237	if comp.CargoOutDir().Valid() {
238		crate.Env["OUT_DIR"] = comp.CargoOutDir().String()
239	}
240
241	for _, feature := range comp.Properties.Features {
242		crate.Cfg = append(crate.Cfg, "feature=\""+feature+"\"")
243	}
244
245	deps := make(map[string]int)
246	singleton.mergeDependencies(ctx, rModule, &crate, deps)
247
248	idx := len(singleton.project.Crates)
249	singleton.knownCrates[rModule.Name()] = crateInfo{Idx: idx, Deps: deps}
250	singleton.project.Crates = append(singleton.project.Crates, crate)
251	// rust-analyzer requires that all crates belong to at least one root:
252	// https://github.com/rust-analyzer/rust-analyzer/issues/4735.
253	singleton.project.Roots = append(singleton.project.Roots, path.Dir(crate.RootModule))
254	return idx, true
255}
256
257// appendCrateAndDependencies creates a rustProjectCrate for the module argument and appends it to singleton.project.
258// It visits the dependencies of the module depth-first so the dependency ID can be added to the current module. If the
259// current module is already in singleton.knownCrates, its dependencies are merged.
260func (singleton *projectGeneratorSingleton) appendCrateAndDependencies(ctx android.SingletonContext, module android.Module) {
261	rModule, comp, ok := isModuleSupported(ctx, module)
262	if !ok {
263		return
264	}
265	// If we have seen this crate already; merge any new dependencies.
266	if cInfo, ok := singleton.knownCrates[module.Name()]; ok {
267		crate := singleton.project.Crates[cInfo.Idx]
268		singleton.mergeDependencies(ctx, rModule, &crate, cInfo.Deps)
269		singleton.project.Crates[cInfo.Idx] = crate
270		return
271	}
272	singleton.addCrate(ctx, rModule, comp)
273}
274
275func (singleton *projectGeneratorSingleton) GenerateBuildActions(ctx android.SingletonContext) {
276	if !ctx.Config().IsEnvTrue(envVariableCollectRustDeps) {
277		return
278	}
279
280	singleton.knownCrates = make(map[string]crateInfo)
281	ctx.VisitAllModules(func(module android.Module) {
282		singleton.appendCrateAndDependencies(ctx, module)
283	})
284
285	path := android.PathForOutput(ctx, rustProjectJsonFileName)
286	err := createJsonFile(singleton.project, path)
287	if err != nil {
288		ctx.Errorf(err.Error())
289	}
290}
291
292func createJsonFile(project rustProjectJson, rustProjectPath android.WritablePath) error {
293	buf, err := json.MarshalIndent(project, "", "  ")
294	if err != nil {
295		return fmt.Errorf("JSON marshal of rustProjectJson failed: %s", err)
296	}
297	err = android.WriteFileToOutputDir(rustProjectPath, buf, 0666)
298	if err != nil {
299		return fmt.Errorf("Writing rust-project to %s failed: %s", rustProjectPath.String(), err)
300	}
301	return nil
302}
303