ink_e2e/
contract_build.rs

1// Copyright (C) Use Ink (UK) Ltd.
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
15use std::{
16    collections::{
17        hash_map::Entry,
18        HashMap,
19    },
20    env,
21    path::{
22        Path,
23        PathBuf,
24    },
25    sync::{
26        Mutex,
27        OnceLock,
28    },
29};
30
31use contract_build::{
32    package_abi,
33    util::rustc_wrapper,
34    Abi,
35    BuildArtifacts,
36    BuildMode,
37    ExecuteArgs,
38    Features,
39    ImageVariant,
40    ManifestPath,
41    Network,
42    OutputType,
43    UnstableFlags,
44    Verbosity,
45};
46use itertools::Itertools;
47
48use crate::log_info;
49
50/// Builds the "root" contract (the contract in which the E2E tests are defined) together
51/// with any contracts which are a dependency of the root contract.
52///
53/// todo explain features
54pub fn build_root_and_contract_dependencies(features: Vec<String>) -> Vec<PathBuf> {
55    let contract_project = ContractProject::new();
56    let contract_manifests = contract_project.root_with_contract_dependencies();
57    if contract_project.package_abi.is_some() {
58        // Generates a custom `rustc` wrapper which passes compiler flags to `rustc`,
59        // because `cargo` doesn't pass compiler flags to proc macros and build
60        // scripts when the `--target` flag is set.
61        // See `contract_build::util::rustc_wrapper::generate` docs for details.
62        if let Ok(rustc_wrapper) = rustc_wrapper::generate(&contract_project.target_dir) {
63            // SAFETY: The `rustc` wrapper is safe to reuse across all threads.
64            env::set_var("INK_RUSTC_WRAPPER", rustc_wrapper);
65        }
66    }
67    build_contracts(&contract_manifests, features)
68}
69
70/// Access manifest paths of contracts which are part of the project in which the E2E
71/// tests are defined.
72struct ContractProject {
73    root_package: Option<PathBuf>,
74    contract_dependencies: Vec<PathBuf>,
75    package_abi: Option<Abi>,
76    target_dir: PathBuf,
77}
78
79impl ContractProject {
80    fn new() -> Self {
81        let mut cmd = cargo_metadata::MetadataCommand::new();
82        let env_target_dir = env::var_os("CARGO_TARGET_DIR")
83            .map(|target_dir| PathBuf::from(target_dir))
84            .filter(|target_dir| target_dir.is_absolute());
85        if let Some(target_dir) = env_target_dir.as_ref() {
86            cmd.env("CARGO_TARGET_DIR", target_dir);
87        }
88        let metadata = cmd
89            .exec()
90            .unwrap_or_else(|err| panic!("Error invoking `cargo metadata`: {}", err));
91
92        fn maybe_contract_package(package: &cargo_metadata::Package) -> Option<PathBuf> {
93            package
94                .features
95                .iter()
96                .any(|(feat, _)| {
97                    feat == "ink-as-dependency"
98                        && !package.name.eq("ink")
99                        && !package.name.eq("ink_env")
100                })
101                .then(|| package.manifest_path.clone().into_std_path_buf())
102        }
103
104        let root_package = metadata
105            .resolve
106            .as_ref()
107            .and_then(|resolve| resolve.root.as_ref())
108            .and_then(|root_package_id| {
109                metadata
110                    .packages
111                    .iter()
112                    .find(|package| &package.id == root_package_id)
113            })
114            .and_then(maybe_contract_package);
115        log_info(&format!("found root package: {:?}", root_package));
116
117        let contract_dependencies: Vec<PathBuf> = metadata
118            .packages
119            .iter()
120            .filter_map(maybe_contract_package)
121            .collect();
122        log_info(&format!(
123            "found those contract dependencies: {:?}",
124            contract_dependencies
125        ));
126
127        let package_abi = metadata
128            .root_package()
129            .and_then(|package| package_abi(package))
130            .and_then(Result::ok);
131        log_info(&format!("found root package abi: {:?}", package_abi));
132
133        let target_dir = env_target_dir
134            .unwrap_or_else(|| metadata.target_directory.into_std_path_buf());
135        log_info(&format!("found target dir: {:?}", target_dir));
136
137        Self {
138            root_package,
139            contract_dependencies,
140            package_abi,
141            target_dir,
142        }
143    }
144
145    fn root_with_additional_contracts<P>(
146        &self,
147        additional_contracts: impl IntoIterator<Item = P>,
148    ) -> Vec<PathBuf>
149    where
150        PathBuf: From<P>,
151    {
152        let mut all_manifests: Vec<_> = self.root_package.iter().cloned().collect();
153        let mut additional_contracts: Vec<_> = additional_contracts
154            .into_iter()
155            .map(PathBuf::from)
156            .collect();
157        all_manifests.append(&mut additional_contracts);
158        all_manifests.into_iter().unique().collect()
159    }
160
161    fn root_with_contract_dependencies(&self) -> Vec<PathBuf> {
162        self.root_with_additional_contracts(&self.contract_dependencies)
163    }
164}
165
166/// Build all the of the contracts of the supplied `contract_manifests`.
167///
168/// Only attempts to build a contract at the given path once only per test run, to avoid
169/// the attempt for different tests to build the same contract concurrently.
170fn build_contracts(
171    contract_manifests: &[PathBuf],
172    features: Vec<String>,
173) -> Vec<PathBuf> {
174    static CONTRACT_BUILD_JOBS: OnceLock<Mutex<HashMap<PathBuf, PathBuf>>> =
175        OnceLock::new();
176    let mut contract_build_jobs = CONTRACT_BUILD_JOBS
177        .get_or_init(|| Mutex::new(HashMap::new()))
178        .lock()
179        .unwrap();
180
181    let mut blob_paths = Vec::new();
182    for manifest in contract_manifests {
183        let contract_binary_path = match contract_build_jobs.entry(manifest.clone()) {
184            Entry::Occupied(entry) => entry.get().clone(),
185            Entry::Vacant(entry) => {
186                let contract_binary_path = build_contract(manifest, features.clone());
187                entry.insert(contract_binary_path.clone());
188                contract_binary_path
189            }
190        };
191        blob_paths.push(contract_binary_path);
192    }
193    blob_paths
194}
195
196/// Builds the contract at `manifest_path`, returns the path to the contract
197/// PolkaVM build artifact.
198fn build_contract(
199    path_to_cargo_toml: &Path,
200    additional_features: Vec<String>,
201) -> PathBuf {
202    let manifest_path = ManifestPath::new(path_to_cargo_toml).unwrap_or_else(|err| {
203        panic!(
204            "Invalid manifest path {}: {err}",
205            path_to_cargo_toml.display()
206        )
207    });
208    // todo add method in Features to just construct with new(features)
209    let mut features = Features::default();
210    additional_features.iter().for_each(|f| features.push(f));
211    let args = ExecuteArgs {
212        manifest_path,
213        verbosity: Verbosity::Default,
214        build_mode: BuildMode::Debug,
215        features,
216        network: Network::Online,
217        build_artifact: BuildArtifacts::All,
218        unstable_flags: UnstableFlags::default(),
219        keep_debug_symbols: false,
220        extra_lints: false,
221        output_type: OutputType::HumanReadable,
222        image: ImageVariant::Default,
223        metadata_spec: None,
224    };
225
226    match contract_build::execute(args) {
227        Ok(build_result) => {
228            build_result
229                .dest_binary
230                .expect("PolkaVM code artifact not generated")
231                .canonicalize()
232                .expect("Invalid dest bundle path")
233        }
234        Err(err) => {
235            panic!(
236                "contract build for {} failed: {err}",
237                path_to_cargo_toml.display()
238            )
239        }
240    }
241}