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 crate::log_info;
32use contract_build::{
33    BuildArtifacts,
34    BuildMode,
35    ExecuteArgs,
36    Features,
37    ImageVariant,
38    ManifestPath,
39    Network,
40    OutputType,
41    UnstableFlags,
42    Verbosity,
43};
44use itertools::Itertools;
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/// Builds the root contract with `features`.
50pub fn build_root_and_contract_dependencies(features: Vec<String>) -> Vec<PathBuf> {
51    let contract_project = ContractProject::new();
52    let contract_manifests_and_features =
53        contract_project.root_with_contract_dependencies(features);
54    build_contracts(
55        &contract_manifests_and_features,
56        contract_project.target_dir,
57    )
58}
59
60/// Access manifest paths of contracts which are part of the project in which the E2E
61/// tests are defined.
62struct ContractProject {
63    root_package: Option<PathBuf>,
64    contract_dependencies: Vec<PathBuf>,
65    target_dir: PathBuf,
66}
67
68impl ContractProject {
69    fn new() -> Self {
70        let mut cmd = cargo_metadata::MetadataCommand::new();
71        let env_target_dir = env::var_os("CARGO_TARGET_DIR")
72            .map(PathBuf::from)
73            .filter(|target_dir| target_dir.is_absolute());
74        if let Some(target_dir) = env_target_dir.as_ref() {
75            cmd.env("CARGO_TARGET_DIR", target_dir);
76        }
77        let metadata = cmd
78            .exec()
79            .unwrap_or_else(|err| panic!("Error invoking `cargo metadata`: {err}"));
80
81        fn maybe_contract_package(package: &cargo_metadata::Package) -> Option<PathBuf> {
82            package
83                .features
84                .iter()
85                .any(|(feat, _)| {
86                    feat == "ink-as-dependency"
87                        && !package.name.as_str().eq("ink")
88                        && !package.name.as_str().eq("ink_env")
89                })
90                .then(|| package.manifest_path.clone().into_std_path_buf())
91        }
92
93        let root_package = metadata
94            .resolve
95            .as_ref()
96            .and_then(|resolve| resolve.root.as_ref())
97            .and_then(|root_package_id| {
98                metadata
99                    .packages
100                    .iter()
101                    .find(|package| &package.id == root_package_id)
102            })
103            .and_then(maybe_contract_package);
104        log_info(&format!("found root package: {root_package:?}"));
105
106        let contract_dependencies: Vec<PathBuf> = metadata
107            .packages
108            .iter()
109            .filter_map(maybe_contract_package)
110            .collect();
111        log_info(&format!(
112            "found those contract dependencies: {contract_dependencies:?}"
113        ));
114
115        let target_dir = env_target_dir
116            .unwrap_or_else(|| metadata.target_directory.into_std_path_buf());
117        log_info(&format!("found target dir: {target_dir:?}"));
118
119        Self {
120            root_package,
121            contract_dependencies,
122            target_dir,
123        }
124    }
125
126    fn root_with_additional_contracts<P>(
127        &self,
128        additional_contracts: impl IntoIterator<Item = P>,
129        features: Vec<String>,
130    ) -> Vec<(PathBuf, Vec<String>)>
131    where
132        PathBuf: From<P>,
133    {
134        let mut all_manifests: Vec<_> = self
135            .root_package
136            .iter()
137            .cloned()
138            .map(|path| (path, features.clone()))
139            .collect();
140        let mut additional_contracts: Vec<_> = additional_contracts
141            .into_iter()
142            .map(PathBuf::from)
143            .map(|path| (path, vec![]))
144            .collect();
145        all_manifests.append(&mut additional_contracts);
146        all_manifests.into_iter().unique().collect()
147    }
148
149    fn root_with_contract_dependencies(
150        &self,
151        features: Vec<String>,
152    ) -> Vec<(PathBuf, Vec<String>)> {
153        self.root_with_additional_contracts(&self.contract_dependencies, features)
154    }
155}
156
157/// Build all contracts of the supplied `contract_manifests`.
158///
159/// Only attempts to build a contract at the given path once only per test run, to avoid
160/// the attempt for different tests to build the same contract concurrently.
161fn build_contracts(
162    contract_manifests: &[(PathBuf, Vec<String>)],
163    target_dir: PathBuf,
164) -> Vec<PathBuf> {
165    static CONTRACT_BUILD_JOBS: OnceLock<
166        Mutex<HashMap<(PathBuf, Vec<String>), PathBuf>>,
167    > = OnceLock::new();
168    let mut contract_build_jobs = CONTRACT_BUILD_JOBS
169        .get_or_init(|| Mutex::new(HashMap::new()))
170        .lock()
171        .unwrap();
172
173    let mut blob_paths = Vec::new();
174    for (manifest, features) in contract_manifests {
175        let key = (manifest.clone(), features.clone());
176        let contract_binary_path = match contract_build_jobs.entry(key) {
177            Entry::Occupied(entry) => entry.get().clone(),
178            Entry::Vacant(entry) => {
179                let contract_binary_path =
180                    build_contract(manifest, features.clone(), target_dir.clone());
181                let path_with_features =
182                    add_features_to_filename(contract_binary_path, features);
183                entry.insert(path_with_features.clone());
184                path_with_features
185            }
186        };
187        blob_paths.push(contract_binary_path);
188    }
189    blob_paths
190}
191
192fn add_features_to_filename(
193    contract_binary_path: PathBuf,
194    features: &Vec<String>,
195) -> PathBuf {
196    // add features to file name
197    let mut path_with_features = contract_binary_path.clone();
198    let filename = path_with_features
199        .file_stem()
200        .expect("no file name")
201        .to_string_lossy()
202        .into_owned();
203    let extension = path_with_features
204        .extension()
205        .expect("no file name")
206        .to_string_lossy()
207        .into_owned();
208    path_with_features.pop();
209
210    let features_str = features.join("-");
211    let mut new_filename =
212        format!("{}-features-{}", filename, features_str.replace("/", "-"));
213    if features.is_empty() {
214        new_filename.push_str("no");
215    }
216    new_filename.push_str(&format!(".{extension}"));
217    path_with_features.push(new_filename);
218    std::fs::copy(contract_binary_path, path_with_features.as_path())
219        .expect("failed copying binary");
220    path_with_features
221}
222
223/// Builds the contract at `manifest_path`, returns the path to the contract
224/// PolkaVM build artifact.
225fn build_contract(
226    cargo_toml: &Path,
227    features: Vec<String>,
228    target_dir: PathBuf,
229) -> PathBuf {
230    let manifest_path = ManifestPath::new(cargo_toml).unwrap_or_else(|err| {
231        panic!("Invalid manifest path {}: {err}", cargo_toml.display())
232    });
233    let args = ExecuteArgs {
234        manifest_path,
235        verbosity: Verbosity::Default,
236        build_mode: BuildMode::Debug,
237        features: Features::from(features),
238        network: Network::Online,
239        build_artifact: BuildArtifacts::CodeOnly,
240        unstable_flags: UnstableFlags::default(),
241        keep_debug_symbols: false,
242        extra_lints: false,
243        output_type: OutputType::HumanReadable,
244        image: ImageVariant::Default,
245        metadata_spec: None,
246        target_dir: Some(target_dir),
247    };
248
249    match contract_build::execute(args) {
250        Ok(build_result) => {
251            build_result
252                .dest_binary
253                .expect("PolkaVM code artifact not generated")
254                .canonicalize()
255                .expect("Invalid dest bundle path")
256        }
257        Err(err) => {
258            panic!("contract build for {} failed: {err}", cargo_toml.display())
259        }
260    }
261}