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