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