1#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6#![cfg_attr(docsrs, feature(doc_cfg))]
7
8use alloy_primitives::{Address, B256, Bytes};
9use foundry_compilers::{
10 Artifact, ArtifactId,
11 artifacts::{CompactContractBytecodeCow, Libraries},
12 contracts::ArtifactContracts,
13};
14use rayon::prelude::*;
15use semver::Version;
16use std::{
17 collections::{BTreeMap, BTreeSet},
18 path::{Path, PathBuf},
19 str::FromStr,
20};
21
22#[derive(Debug, thiserror::Error)]
24pub enum LinkerError {
25 #[error("wasn't able to find artifact for library {name} at {file}")]
26 MissingLibraryArtifact { file: String, name: String },
27 #[error("target artifact is not present in provided artifacts set")]
28 MissingTargetArtifact,
29 #[error(transparent)]
30 InvalidAddress(<Address as std::str::FromStr>::Err),
31 #[error("cyclic dependency found, can't link libraries via CREATE2")]
32 CyclicDependency,
33 #[error("failed linking {artifact}")]
34 LinkingFailed { artifact: String },
35}
36
37pub struct Linker<'a> {
38 pub root: PathBuf,
40 pub contracts: ArtifactContracts<CompactContractBytecodeCow<'a>>,
42}
43
44pub struct LinkOutput {
46 pub libraries: Libraries,
49 pub libs_to_deploy: Vec<Bytes>,
52}
53
54impl<'a> Linker<'a> {
55 pub fn new(
56 root: impl Into<PathBuf>,
57 contracts: ArtifactContracts<CompactContractBytecodeCow<'a>>,
58 ) -> Self {
59 Linker { root: root.into(), contracts }
60 }
61
62 fn convert_artifact_id_to_lib_path(&self, id: &ArtifactId) -> (PathBuf, String) {
67 let path = id.source.strip_prefix(self.root.as_path()).unwrap_or(&id.source);
68 let name = id.name.split('.').next().unwrap();
70
71 (path.to_path_buf(), name.to_owned())
72 }
73
74 fn find_artifact_id_by_library_path(
79 &'a self,
80 file: &str,
81 name: &str,
82 version: Option<&Version>,
83 ) -> Option<&'a ArtifactId> {
84 for id in self.contracts.keys() {
85 if let Some(version) = version
86 && id.version != *version
87 {
88 continue;
89 }
90 let (artifact_path, artifact_name) = self.convert_artifact_id_to_lib_path(id);
91
92 if artifact_name == *name && artifact_path == Path::new(file) {
93 return Some(id);
94 }
95 }
96
97 None
98 }
99
100 fn collect_dependencies(
102 &'a self,
103 target: &'a ArtifactId,
104 deps: &mut BTreeSet<&'a ArtifactId>,
105 ) -> Result<(), LinkerError> {
106 let contract = self.contracts.get(target).ok_or(LinkerError::MissingTargetArtifact)?;
107
108 let mut references = BTreeMap::new();
109 if let Some(bytecode) = &contract.bytecode {
110 references.extend(bytecode.link_references.clone());
111 }
112 if let Some(deployed_bytecode) = &contract.deployed_bytecode
113 && let Some(bytecode) = &deployed_bytecode.bytecode
114 {
115 references.extend(bytecode.link_references.clone());
116 }
117
118 for (file, libs) in &references {
119 for contract in libs.keys() {
120 let id = self
121 .find_artifact_id_by_library_path(file, contract, Some(&target.version))
122 .ok_or_else(|| LinkerError::MissingLibraryArtifact {
123 file: file.to_string(),
124 name: contract.to_string(),
125 })?;
126 if deps.insert(id) {
127 self.collect_dependencies(id, deps)?;
128 }
129 }
130 }
131
132 Ok(())
133 }
134
135 pub fn link_with_nonce_or_address(
145 &'a self,
146 libraries: Libraries,
147 sender: Address,
148 mut nonce: u64,
149 targets: impl IntoIterator<Item = &'a ArtifactId>,
150 ) -> Result<LinkOutput, LinkerError> {
151 let mut libraries = libraries.with_stripped_file_prefixes(self.root.as_path());
154
155 let mut needed_libraries = BTreeSet::new();
156 for target in targets {
157 self.collect_dependencies(target, &mut needed_libraries)?;
158 }
159
160 let mut libs_to_deploy = Vec::new();
161
162 for id in needed_libraries {
165 let (lib_path, lib_name) = self.convert_artifact_id_to_lib_path(id);
166
167 libraries.libs.entry(lib_path).or_default().entry(lib_name).or_insert_with(|| {
168 let address = sender.create(nonce);
169 libs_to_deploy.push((id, address));
170 nonce += 1;
171
172 address.to_checksum(None)
173 });
174 }
175
176 let libs_to_deploy = libs_to_deploy
178 .into_par_iter()
179 .map(|(id, _)| {
180 Ok(self.link(id, &libraries)?.get_bytecode_bytes().unwrap().into_owned())
181 })
182 .collect::<Result<Vec<_>, LinkerError>>()?;
183
184 Ok(LinkOutput { libraries, libs_to_deploy })
185 }
186
187 pub fn link_with_create2(
188 &'a self,
189 libraries: Libraries,
190 sender: Address,
191 salt: B256,
192 target: &'a ArtifactId,
193 ) -> Result<LinkOutput, LinkerError> {
194 let mut libraries = libraries.with_stripped_file_prefixes(self.root.as_path());
197
198 let mut needed_libraries = BTreeSet::new();
199 self.collect_dependencies(target, &mut needed_libraries)?;
200
201 let mut needed_libraries = needed_libraries
202 .into_par_iter()
203 .filter(|id| {
204 let (file, name) = self.convert_artifact_id_to_lib_path(id);
206 !libraries.libs.contains_key(&file) || !libraries.libs[&file].contains_key(&name)
207 })
208 .map(|id| {
209 let bytecode = self.link(id, &libraries).unwrap().bytecode.unwrap();
211 (id, bytecode)
212 })
213 .collect::<Vec<_>>();
214
215 let mut libs_to_deploy = Vec::new();
216
217 while !needed_libraries.is_empty() {
220 let deployable = needed_libraries
222 .iter()
223 .enumerate()
224 .find(|(_, (_, bytecode))| !bytecode.object.is_unlinked());
225
226 let Some((index, &(id, _))) = deployable else {
228 return Err(LinkerError::CyclicDependency);
229 };
230 let (_, bytecode) = needed_libraries.swap_remove(index);
231 let code = bytecode.bytes().unwrap();
232 let address = sender.create2_from_code(salt, code);
233 libs_to_deploy.push(code.clone());
234
235 let (file, name) = self.convert_artifact_id_to_lib_path(id);
236
237 needed_libraries.par_iter_mut().for_each(|(_, bytecode)| {
238 bytecode.to_mut().link(&file.to_string_lossy(), &name, address);
239 });
240
241 libraries.libs.entry(file).or_default().insert(name, address.to_checksum(None));
242 }
243
244 Ok(LinkOutput { libraries, libs_to_deploy })
245 }
246
247 pub fn link(
249 &self,
250 target: &ArtifactId,
251 libraries: &Libraries,
252 ) -> Result<CompactContractBytecodeCow<'a>, LinkerError> {
253 let mut contract =
254 self.contracts.get(target).ok_or(LinkerError::MissingTargetArtifact)?.clone();
255 for (file, libs) in &libraries.libs {
256 for (name, address) in libs {
257 let address = Address::from_str(address).map_err(LinkerError::InvalidAddress)?;
258 if let Some(bytecode) = contract.bytecode.as_mut() {
259 bytecode.to_mut().link(&file.to_string_lossy(), name, address);
260 }
261 if let Some(deployed_bytecode) =
262 contract.deployed_bytecode.as_mut().and_then(|b| b.to_mut().bytecode.as_mut())
263 {
264 deployed_bytecode.link(&file.to_string_lossy(), name, address);
265 }
266 }
267 }
268 Ok(contract)
269 }
270
271 pub fn ensure_linked(
273 &self,
274 contract: &CompactContractBytecodeCow<'a>,
275 target: &ArtifactId,
276 ) -> Result<(), LinkerError> {
277 if let Some(bytecode) = &contract.bytecode
278 && bytecode.object.is_unlinked()
279 {
280 return Err(LinkerError::LinkingFailed {
281 artifact: target.source.to_string_lossy().into(),
282 });
283 }
284 if let Some(deployed_bytecode) = &contract.deployed_bytecode
285 && let Some(deployed_bytecode_obj) = &deployed_bytecode.bytecode
286 && deployed_bytecode_obj.object.is_unlinked()
287 {
288 return Err(LinkerError::LinkingFailed {
289 artifact: target.source.to_string_lossy().into(),
290 });
291 }
292 Ok(())
293 }
294
295 pub fn get_linked_artifacts(
296 &self,
297 libraries: &Libraries,
298 ) -> Result<ArtifactContracts, LinkerError> {
299 self.get_linked_artifacts_cow(libraries).map(ArtifactContracts::from_iter)
300 }
301
302 pub fn get_linked_artifacts_cow(
303 &self,
304 libraries: &Libraries,
305 ) -> Result<ArtifactContracts<CompactContractBytecodeCow<'a>>, LinkerError> {
306 self.contracts
307 .par_iter()
308 .map(|(id, _)| Ok((id.clone(), self.link(id, libraries)?)))
309 .collect::<Result<_, _>>()
310 .map(ArtifactContracts)
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use alloy_primitives::{address, fixed_bytes, map::HashMap};
318 use foundry_compilers::{
319 Project, ProjectCompileOutput, ProjectPathsConfig,
320 multi::MultiCompiler,
321 solc::{Solc, SolcCompiler},
322 };
323 use std::sync::OnceLock;
324
325 fn testdata() -> &'static Path {
326 static CACHE: OnceLock<PathBuf> = OnceLock::new();
327 CACHE.get_or_init(|| {
328 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../testdata").canonicalize().unwrap()
329 })
330 }
331
332 #[must_use]
333 struct LinkerTest {
334 project: Project,
335 output: ProjectCompileOutput,
336 dependency_assertions: HashMap<&'static str, Vec<(&'static str, Address)>>,
337 }
338
339 impl LinkerTest {
340 fn new(path: &Path, strip_prefixes: bool) -> Self {
341 assert!(path.exists(), "Path {path:?} does not exist");
342 let paths = ProjectPathsConfig::builder()
343 .root(testdata())
344 .lib(testdata().join("lib"))
345 .sources(path)
346 .tests(path)
347 .build()
348 .unwrap();
349
350 let solc = Solc::find_or_install(&Version::new(0, 8, 18)).unwrap();
351 let project = Project::builder()
352 .paths(paths)
353 .ephemeral()
354 .no_artifacts()
355 .build(MultiCompiler { solc: Some(SolcCompiler::Specific(solc)), vyper: None })
356 .unwrap();
357
358 let mut output = project.compile().unwrap();
359
360 if strip_prefixes {
361 output = output.with_stripped_file_prefixes(project.root());
362 }
363
364 Self { project, output, dependency_assertions: HashMap::default() }
365 }
366
367 fn assert_dependencies(
368 mut self,
369 artifact_id: &'static str,
370 deps: &[(&'static str, Address)],
371 ) -> Self {
372 self.dependency_assertions.insert(artifact_id, deps.to_vec());
373 self
374 }
375
376 fn test_with_sender_and_nonce(self, sender: Address, initial_nonce: u64) {
377 let linker = Linker::new(self.project.root(), self.output.artifact_ids().collect());
378 for (id, identifier) in self.iter_linking_targets(&linker) {
379 let output = linker
380 .link_with_nonce_or_address(Default::default(), sender, initial_nonce, [id])
381 .expect("Linking failed");
382 self.validate_assertions(identifier, output);
383 }
384 }
385
386 fn test_with_create2(self, sender: Address, salt: B256) {
387 let linker = Linker::new(self.project.root(), self.output.artifact_ids().collect());
388 for (id, identifier) in self.iter_linking_targets(&linker) {
389 let output = linker
390 .link_with_create2(Default::default(), sender, salt, id)
391 .expect("Linking failed");
392 self.validate_assertions(identifier, output);
393 }
394 }
395
396 fn iter_linking_targets<'a>(
397 &'a self,
398 linker: &'a Linker<'_>,
399 ) -> impl Iterator<Item = (&'a ArtifactId, String)> + 'a {
400 self.sanity_check(linker);
401 linker.contracts.keys().filter_map(move |id| {
402 let source = id
406 .source
407 .strip_prefix(self.project.root())
408 .unwrap_or(&id.source)
409 .to_string_lossy();
410 let identifier = format!("{source}:{}", id.name);
411
412 if identifier.contains("DSTest") {
415 return None;
416 }
417
418 Some((id, identifier))
419 })
420 }
421
422 fn sanity_check(&self, linker: &Linker<'_>) {
423 assert!(!self.dependency_assertions.is_empty(), "Dependency assertions are empty");
424 assert!(!linker.contracts.is_empty(), "Linker contracts are empty");
425 }
426
427 fn validate_assertions(&self, identifier: String, output: LinkOutput) {
428 let LinkOutput { libs_to_deploy, libraries } = output;
429
430 let assertions = self
431 .dependency_assertions
432 .get(identifier.as_str())
433 .unwrap_or_else(|| panic!("Unexpected artifact: {identifier}"));
434
435 assert_eq!(
436 libs_to_deploy.len(),
437 assertions.len(),
438 "artifact {identifier} has more/less dependencies than expected ({} vs {}): {:#?}",
439 libs_to_deploy.len(),
440 assertions.len(),
441 libs_to_deploy
442 );
443
444 for &(dep_identifier, address) in assertions {
445 let (file, name) = dep_identifier.split_once(':').unwrap();
446 if let Some(lib_address) =
447 libraries.libs.get(Path::new(file)).and_then(|libs| libs.get(name))
448 {
449 assert_eq!(
450 lib_address.parse::<Address>().unwrap(),
451 address,
452 "incorrect library address for dependency {dep_identifier} of {identifier}"
453 );
454 } else {
455 panic!("Library {dep_identifier} not found");
456 }
457 }
458 }
459 }
460
461 fn link_test(path: impl AsRef<Path>, mut test_fn: impl FnMut(LinkerTest)) {
462 fn link_test(path: &Path, test_fn: &mut dyn FnMut(LinkerTest)) {
463 test_fn(LinkerTest::new(path, true));
464 test_fn(LinkerTest::new(path, false));
465 }
466 link_test(path.as_ref(), &mut test_fn);
467 }
468
469 #[test]
470 #[should_panic = "assertions are empty"]
471 fn no_assertions() {
472 link_test(testdata().join("default/linking/simple"), |linker| {
473 linker.test_with_sender_and_nonce(Address::default(), 1);
474 });
475 }
476
477 #[test]
478 #[should_panic = "does not exist"]
479 fn unknown_path() {
480 link_test("doesnotexist", |linker| {
481 linker
482 .assert_dependencies("a:b", &[])
483 .test_with_sender_and_nonce(Address::default(), 1);
484 });
485 }
486
487 #[test]
488 fn link_simple() {
489 link_test(testdata().join("default/linking/simple"), |linker| {
490 linker
491 .assert_dependencies("default/linking/simple/Simple.t.sol:Lib", &[])
492 .assert_dependencies(
493 "default/linking/simple/Simple.t.sol:LibraryConsumer",
494 &[(
495 "default/linking/simple/Simple.t.sol:Lib",
496 address!("0x5a443704dd4b594b382c22a083e2bd3090a6fef3"),
497 )],
498 )
499 .assert_dependencies(
500 "default/linking/simple/Simple.t.sol:SimpleLibraryLinkingTest",
501 &[(
502 "default/linking/simple/Simple.t.sol:Lib",
503 address!("0x5a443704dd4b594b382c22a083e2bd3090a6fef3"),
504 )],
505 )
506 .test_with_sender_and_nonce(Address::default(), 1);
507 });
508 }
509
510 #[test]
511 fn link_nested() {
512 link_test(testdata().join("default/linking/nested"), |linker| {
513 linker
514 .assert_dependencies("default/linking/nested/Nested.t.sol:Lib", &[])
515 .assert_dependencies(
516 "default/linking/nested/Nested.t.sol:NestedLib",
517 &[(
518 "default/linking/nested/Nested.t.sol:Lib",
519 address!("0x5a443704dd4b594b382c22a083e2bd3090a6fef3"),
520 )],
521 )
522 .assert_dependencies(
523 "default/linking/nested/Nested.t.sol:LibraryConsumer",
524 &[
525 (
528 "default/linking/nested/Nested.t.sol:Lib",
529 Address::from_str("0x5a443704dd4b594b382c22a083e2bd3090a6fef3")
530 .unwrap(),
531 ),
532 (
533 "default/linking/nested/Nested.t.sol:NestedLib",
534 Address::from_str("0x47e9Fbef8C83A1714F1951F142132E6e90F5fa5D")
535 .unwrap(),
536 ),
537 ],
538 )
539 .assert_dependencies(
540 "default/linking/nested/Nested.t.sol:NestedLibraryLinkingTest",
541 &[
542 (
543 "default/linking/nested/Nested.t.sol:Lib",
544 Address::from_str("0x5a443704dd4b594b382c22a083e2bd3090a6fef3")
545 .unwrap(),
546 ),
547 (
548 "default/linking/nested/Nested.t.sol:NestedLib",
549 Address::from_str("0x47e9fbef8c83a1714f1951f142132e6e90f5fa5d")
550 .unwrap(),
551 ),
552 ],
553 )
554 .test_with_sender_and_nonce(Address::default(), 1);
555 });
556 }
557
558 #[test]
559 fn link_duplicate() {
560 link_test(testdata().join("default/linking/duplicate"), |linker| {
561 linker
562 .assert_dependencies("default/linking/duplicate/Duplicate.t.sol:A", &[])
563 .assert_dependencies("default/linking/duplicate/Duplicate.t.sol:B", &[])
564 .assert_dependencies(
565 "default/linking/duplicate/Duplicate.t.sol:C",
566 &[(
567 "default/linking/duplicate/Duplicate.t.sol:A",
568 address!("0x5a443704dd4b594b382c22a083e2bd3090a6fef3"),
569 )],
570 )
571 .assert_dependencies(
572 "default/linking/duplicate/Duplicate.t.sol:D",
573 &[(
574 "default/linking/duplicate/Duplicate.t.sol:B",
575 address!("0x5a443704dd4b594b382c22a083e2bd3090a6fef3"),
576 )],
577 )
578 .assert_dependencies(
579 "default/linking/duplicate/Duplicate.t.sol:E",
580 &[
581 (
582 "default/linking/duplicate/Duplicate.t.sol:A",
583 Address::from_str("0x5a443704dd4b594b382c22a083e2bd3090a6fef3")
584 .unwrap(),
585 ),
586 (
587 "default/linking/duplicate/Duplicate.t.sol:C",
588 Address::from_str("0x47e9fbef8c83a1714f1951f142132e6e90f5fa5d")
589 .unwrap(),
590 ),
591 ],
592 )
593 .assert_dependencies(
594 "default/linking/duplicate/Duplicate.t.sol:LibraryConsumer",
595 &[
596 (
597 "default/linking/duplicate/Duplicate.t.sol:A",
598 Address::from_str("0x5a443704dd4b594b382c22a083e2bd3090a6fef3")
599 .unwrap(),
600 ),
601 (
602 "default/linking/duplicate/Duplicate.t.sol:B",
603 Address::from_str("0x47e9fbef8c83a1714f1951f142132e6e90f5fa5d")
604 .unwrap(),
605 ),
606 (
607 "default/linking/duplicate/Duplicate.t.sol:C",
608 Address::from_str("0x8be503bcded90ed42eff31f56199399b2b0154ca")
609 .unwrap(),
610 ),
611 (
612 "default/linking/duplicate/Duplicate.t.sol:D",
613 Address::from_str("0x47c5e40890bce4a473a49d7501808b9633f29782")
614 .unwrap(),
615 ),
616 (
617 "default/linking/duplicate/Duplicate.t.sol:E",
618 Address::from_str("0x29b2440db4a256b0c1e6d3b4cdcaa68e2440a08f")
619 .unwrap(),
620 ),
621 ],
622 )
623 .assert_dependencies(
624 "default/linking/duplicate/Duplicate.t.sol:DuplicateLibraryLinkingTest",
625 &[
626 (
627 "default/linking/duplicate/Duplicate.t.sol:A",
628 Address::from_str("0x5a443704dd4b594b382c22a083e2bd3090a6fef3")
629 .unwrap(),
630 ),
631 (
632 "default/linking/duplicate/Duplicate.t.sol:B",
633 Address::from_str("0x47e9fbef8c83a1714f1951f142132e6e90f5fa5d")
634 .unwrap(),
635 ),
636 (
637 "default/linking/duplicate/Duplicate.t.sol:C",
638 Address::from_str("0x8be503bcded90ed42eff31f56199399b2b0154ca")
639 .unwrap(),
640 ),
641 (
642 "default/linking/duplicate/Duplicate.t.sol:D",
643 Address::from_str("0x47c5e40890bce4a473a49d7501808b9633f29782")
644 .unwrap(),
645 ),
646 (
647 "default/linking/duplicate/Duplicate.t.sol:E",
648 Address::from_str("0x29b2440db4a256b0c1e6d3b4cdcaa68e2440a08f")
649 .unwrap(),
650 ),
651 ],
652 )
653 .test_with_sender_and_nonce(Address::default(), 1);
654 });
655 }
656
657 #[test]
658 fn link_cycle() {
659 link_test(testdata().join("default/linking/cycle"), |linker| {
660 linker
661 .assert_dependencies(
662 "default/linking/cycle/Cycle.t.sol:Foo",
663 &[
664 (
665 "default/linking/cycle/Cycle.t.sol:Foo",
666 Address::from_str("0x47e9Fbef8C83A1714F1951F142132E6e90F5fa5D")
667 .unwrap(),
668 ),
669 (
670 "default/linking/cycle/Cycle.t.sol:Bar",
671 Address::from_str("0x5a443704dd4B594B382c22a083e2BD3090A6feF3")
672 .unwrap(),
673 ),
674 ],
675 )
676 .assert_dependencies(
677 "default/linking/cycle/Cycle.t.sol:Bar",
678 &[
679 (
680 "default/linking/cycle/Cycle.t.sol:Foo",
681 Address::from_str("0x47e9Fbef8C83A1714F1951F142132E6e90F5fa5D")
682 .unwrap(),
683 ),
684 (
685 "default/linking/cycle/Cycle.t.sol:Bar",
686 Address::from_str("0x5a443704dd4B594B382c22a083e2BD3090A6feF3")
687 .unwrap(),
688 ),
689 ],
690 )
691 .test_with_sender_and_nonce(Address::default(), 1);
692 });
693 }
694
695 #[test]
696 fn link_create2_nested() {
697 link_test(testdata().join("default/linking/nested"), |linker| {
698 linker
699 .assert_dependencies("default/linking/nested/Nested.t.sol:Lib", &[])
700 .assert_dependencies(
701 "default/linking/nested/Nested.t.sol:NestedLib",
702 &[(
703 "default/linking/nested/Nested.t.sol:Lib",
704 address!("0xddb1Cd2497000DAeA687CEa3dc34Af44084BEa74"),
705 )],
706 )
707 .assert_dependencies(
708 "default/linking/nested/Nested.t.sol:LibraryConsumer",
709 &[
710 (
713 "default/linking/nested/Nested.t.sol:Lib",
714 Address::from_str("0xddb1Cd2497000DAeA687CEa3dc34Af44084BEa74")
715 .unwrap(),
716 ),
717 (
718 "default/linking/nested/Nested.t.sol:NestedLib",
719 Address::from_str("0xfebE2F30641170642f317Ff6F644Cee60E7Ac369")
720 .unwrap(),
721 ),
722 ],
723 )
724 .assert_dependencies(
725 "default/linking/nested/Nested.t.sol:NestedLibraryLinkingTest",
726 &[
727 (
728 "default/linking/nested/Nested.t.sol:Lib",
729 Address::from_str("0xddb1Cd2497000DAeA687CEa3dc34Af44084BEa74")
730 .unwrap(),
731 ),
732 (
733 "default/linking/nested/Nested.t.sol:NestedLib",
734 Address::from_str("0xfebE2F30641170642f317Ff6F644Cee60E7Ac369")
735 .unwrap(),
736 ),
737 ],
738 )
739 .test_with_create2(
740 Address::default(),
741 fixed_bytes!(
742 "19bf59b7b67ae8edcbc6e53616080f61fa99285c061450ad601b0bc40c9adfc9"
743 ),
744 );
745 });
746 }
747
748 #[test]
749 fn linking_failure() {
750 let linker = LinkerTest::new(&testdata().join("default/linking/simple"), true);
751 let linker_instance =
752 Linker::new(linker.project.root(), linker.output.artifact_ids().collect());
753
754 let mut libraries = Libraries::default();
756 libraries.libs.entry("default/linking/simple/Simple.t.sol".into()).or_default().insert(
757 "NonExistentLib".to_string(),
758 "0x5a443704dd4b594b382c22a083e2bd3090a6fef3".to_string(),
759 );
760
761 let artifact_id = linker_instance
763 .contracts
764 .keys()
765 .find(|id| id.name == "LibraryConsumer")
766 .expect("LibraryConsumer contract not found");
767
768 let contract = linker_instance.contracts.get(artifact_id).unwrap();
769
770 assert!(
772 linker_instance.ensure_linked(contract, artifact_id).is_err(),
773 "Expected artifact to have unlinked bytecode"
774 );
775 }
776}