From e4d12a04684ad7792f797fb676541af99311d941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thi=C3=A9baud=20Weksteen?= Date: Fri, 5 Jun 2020 11:09:27 +0200 Subject: [PATCH] Add rust-project.json generator Because we are not relying on Cargo.toml for our crate dependencies, we need to provide a structured file to rust-analyzer which describes the locations of the crates. Add a generator for that purpose, similarly to cc/compdb and cc/ccdeps. Bug: 156395307 Test: SOONG_GEN_RUST_PROJECT=1 m nothing && \ cat ${ANDROID_BUILD_TOP}/out/soong/rust-project.json Change-Id: I46efe0adeddae281eaf86707504c3aa15b5e80b8 --- rust/Android.bp | 2 + rust/project_json.go | 161 ++++++++++++++++++++++++++++++++++++++ rust/project_json_test.go | 55 +++++++++++++ rust/testing.go | 1 + 4 files changed, 219 insertions(+) create mode 100644 rust/project_json.go create mode 100644 rust/project_json_test.go diff --git a/rust/Android.bp b/rust/Android.bp index 684db0bdd..b06ea8e75 100644 --- a/rust/Android.bp +++ b/rust/Android.bp @@ -16,6 +16,7 @@ bootstrap_go_package { "library.go", "prebuilt.go", "proc_macro.go", + "project_json.go", "rust.go", "test.go", "testing.go", @@ -25,6 +26,7 @@ bootstrap_go_package { "compiler_test.go", "coverage_test.go", "library_test.go", + "project_json_test.go", "rust_test.go", "test_test.go", ], diff --git a/rust/project_json.go b/rust/project_json.go new file mode 100644 index 000000000..909aebc98 --- /dev/null +++ b/rust/project_json.go @@ -0,0 +1,161 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rust + +import ( + "encoding/json" + "fmt" + "path" + + "android/soong/android" +) + +// This singleton collects Rust crate definitions and generates a JSON file +// (${OUT_DIR}/soong/rust-project.json) which can be use by external tools, +// such as rust-analyzer. It does so when either make, mm, mma, mmm or mmma is +// called. This singleton is enabled only if SOONG_GEN_RUST_PROJECT is set. +// For example, +// +// $ SOONG_GEN_RUST_PROJECT=1 m nothing + +func init() { + android.RegisterSingletonType("rust_project_generator", rustProjectGeneratorSingleton) +} + +func rustProjectGeneratorSingleton() android.Singleton { + return &projectGeneratorSingleton{} +} + +type projectGeneratorSingleton struct{} + +const ( + // Environment variables used to control the behavior of this singleton. + envVariableCollectRustDeps = "SOONG_GEN_RUST_PROJECT" + rustProjectJsonFileName = "rust-project.json" +) + +// The format of rust-project.json is not yet finalized. A current description is available at: +// https://github.com/rust-analyzer/rust-analyzer/blob/master/docs/user/manual.adoc#non-cargo-based-projects +type rustProjectDep struct { + Crate int `json:"crate"` + Name string `json:"name"` +} + +type rustProjectCrate struct { + RootModule string `json:"root_module"` + Edition string `json:"edition,omitempty"` + Deps []rustProjectDep `json:"deps"` + Cfgs []string `json:"cfgs"` +} + +type rustProjectJson struct { + Roots []string `json:"roots"` + Crates []rustProjectCrate `json:"crates"` +} + +// crateInfo is used during the processing to keep track of the known crates. +type crateInfo struct { + ID int + Deps map[string]int +} + +func mergeDependencies(ctx android.SingletonContext, project *rustProjectJson, + knownCrates map[string]crateInfo, module android.Module, + crate *rustProjectCrate, deps map[string]int) { + + //TODO(tweek): The stdlib dependencies do not appear here. We need to manually add them. + ctx.VisitDirectDeps(module, func(child android.Module) { + childId, childName, ok := appendLibraryAndDeps(ctx, project, knownCrates, child) + if !ok { + return + } + if _, ok = deps[childName]; ok { + return + } + crate.Deps = append(crate.Deps, rustProjectDep{Crate: childId, Name: childName}) + deps[childName] = childId + }) +} + +// appendLibraryAndDeps creates a rustProjectCrate for the module argument and +// appends it to the rustProjectJson struct. It visits the dependencies of the +// module depth-first. If the current module is already in knownCrates, its +// its dependencies are merged. Returns a tuple (id, crate_name, ok). +func appendLibraryAndDeps(ctx android.SingletonContext, project *rustProjectJson, + knownCrates map[string]crateInfo, module android.Module) (int, string, bool) { + rModule, ok := module.(*Module) + if !ok { + return 0, "", false + } + if rModule.compiler == nil { + return 0, "", false + } + rustLib, ok := rModule.compiler.(*libraryDecorator) + if !ok { + return 0, "", false + } + crateName := rModule.CrateName() + if cInfo, ok := knownCrates[crateName]; ok { + // We have seen this crate already; merge any new dependencies. + crate := project.Crates[cInfo.ID] + mergeDependencies(ctx, project, knownCrates, module, &crate, cInfo.Deps) + return cInfo.ID, crateName, true + } + crate := rustProjectCrate{Deps: make([]rustProjectDep, 0), Cfgs: make([]string, 0)} + src := rustLib.Properties.Srcs[0] + crate.RootModule = path.Join(ctx.ModuleDir(rModule), src) + crate.Edition = getEdition(rustLib.baseCompiler) + + deps := make(map[string]int) + mergeDependencies(ctx, project, knownCrates, module, &crate, deps) + + id := len(project.Crates) + knownCrates[crateName] = crateInfo{ID: id, Deps: deps} + project.Crates = append(project.Crates, crate) + // rust-analyzer requires that all crates belong to at least one root: + // https://github.com/rust-analyzer/rust-analyzer/issues/4735. + project.Roots = append(project.Roots, path.Dir(crate.RootModule)) + return id, crateName, true +} + +func (r *projectGeneratorSingleton) GenerateBuildActions(ctx android.SingletonContext) { + if !ctx.Config().IsEnvTrue(envVariableCollectRustDeps) { + return + } + + project := rustProjectJson{} + knownCrates := make(map[string]crateInfo) + ctx.VisitAllModules(func(module android.Module) { + appendLibraryAndDeps(ctx, &project, knownCrates, module) + }) + + path := android.PathForOutput(ctx, rustProjectJsonFileName) + err := createJsonFile(project, path) + if err != nil { + ctx.Errorf(err.Error()) + } +} + +func createJsonFile(project rustProjectJson, rustProjectPath android.WritablePath) error { + buf, err := json.MarshalIndent(project, "", " ") + if err != nil { + return fmt.Errorf("JSON marshal of rustProjectJson failed: %s", err) + } + err = android.WriteFileToOutputDir(rustProjectPath, buf, 0666) + if err != nil { + return fmt.Errorf("Writing rust-project to %s failed: %s", rustProjectPath.String(), err) + } + return nil +} diff --git a/rust/project_json_test.go b/rust/project_json_test.go new file mode 100644 index 000000000..6786e72c7 --- /dev/null +++ b/rust/project_json_test.go @@ -0,0 +1,55 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rust + +import ( + "io/ioutil" + "path/filepath" + "testing" + + "android/soong/android" + "android/soong/cc" +) + +func TestProjectJson(t *testing.T) { + bp := `rust_library { + name: "liba", + srcs: ["src/lib.rs"], + crate_name: "a" + }` + GatherRequiredDepsForTest() + env := map[string]string{"SOONG_GEN_RUST_PROJECT": "1"} + fs := map[string][]byte{ + "foo.rs": nil, + "src/lib.rs": nil, + } + + cc.GatherRequiredFilesForTest(fs) + + config := android.TestArchConfig(buildDir, env, bp, fs) + ctx := CreateTestContext() + ctx.Register(config) + _, errs := ctx.ParseFileList(".", []string{"Android.bp"}) + android.FailIfErrored(t, errs) + _, errs = ctx.PrepareBuildActions(config) + android.FailIfErrored(t, errs) + + // The JSON file is generated via WriteFileToOutputDir. Therefore, it + // won't appear in the Output of the TestingSingleton. Manually verify + // it exists. + _, err := ioutil.ReadFile(filepath.Join(buildDir, "rust-project.json")) + if err != nil { + t.Errorf("rust-project.json has not been generated") + } +} diff --git a/rust/testing.go b/rust/testing.go index 09008a85f..f94af71ee 100644 --- a/rust/testing.go +++ b/rust/testing.go @@ -100,6 +100,7 @@ func CreateTestContext() *android.TestContext { ctx.BottomUp("rust_unit_tests", TestPerSrcMutator).Parallel() ctx.BottomUp("rust_begin", BeginMutator).Parallel() }) + ctx.RegisterSingletonType("rust_project_generator", rustProjectGeneratorSingleton) return ctx }