모듈과 워크스페이스 문서의 검증 코드다.
//! 모듈과 가시성(pub, pub(crate), pub(super), pub use)을 인라인 mod로 확인하고,
//! 컴파일 오류 코드(E0603, E0616, E0451)와 크레이트 경계를 넘는 pub(crate), 기능 플래그(cfg feature),
//! 워크스페이스([workspace.dependencies], [workspace.lints])를 rustc·cargo로 실제로 돌려 확인한다.
//! 의존성: 없음 (표준 라이브러리만)
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/3.개념집/41_modules-workspaces_verify.rs"
use std::path::{Path, PathBuf};
use std::process::Command;
mod shop {
// 바깥에 보여 줄 이름만 다시 내보낸다(re-export). 사용자는 shop::Item으로 쓴다.
pub use self::catalog::Item;
pub mod catalog {
pub struct Item {
pub name: String,
price_cents: u32, // 필드는 따로 pub을 붙여야 보인다
}
impl Item {
pub fn new(name: &str, price_cents: u32) -> Item {
Item {
name: name.to_string(),
price_cents,
}
}
pub fn price(&self) -> u32 {
super::pricing::discounted(self.price_cents)
}
}
/// 이 크레이트 안 어디서나 보인다. 다른 크레이트에서는 안 보인다.
pub(crate) fn count_for_metrics() -> usize {
1
}
}
// pub이 없으니 shop과 그 자손만 본다.
mod pricing {
pub(super) fn discounted(cents: u32) -> u32 {
cents - cents / 10
}
}
}
fn rustc(dir: &Path, file: &str, code: &str, extra: &[&str]) -> (bool, String) {
let f = dir.join(file);
std::fs::write(&f, code).expect("write");
let out = Command::new("rustc")
.args(["--edition", "2024", "--emit=metadata", "--out-dir"])
.arg(dir)
.args(extra)
.arg(&f)
.output()
.expect("rustc 실행");
(
out.status.success(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
fn tmp(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("vr_mod_{}_{name}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).expect("mkdir");
d
}
const SHOP: &str = r#"
#![allow(dead_code)]
pub mod shop {
pub use self::catalog::Item;
pub mod catalog {
pub struct Item { pub name: String, price_cents: u32 }
impl Item { pub fn new(n: &str, p: u32) -> Item { Item { name: n.into(), price_cents: p } } }
pub(crate) fn count_for_metrics() -> usize { 1 }
}
mod pricing { pub(super) fn discounted(c: u32) -> u32 { c } }
}
"#;
fn write(root: &Path, rel: &str, s: &str) {
let f = root.join(rel);
std::fs::create_dir_all(f.parent().expect("parent")).expect("mkdir");
std::fs::write(f, s).expect("write");
}
/// 워크스페이스 두 멤버: core(라이브러리)와 app(바이너리). 공통 의존성과 린트를 루트에서 정한다.
fn workspace_demo(with_unsafe: bool) -> (bool, String, PathBuf) {
let root = tmp(if with_unsafe { "ws_unsafe" } else { "ws" });
write(
&root,
"Cargo.toml",
r#"[workspace]
members = ["crates/*"]
resolver = "3"
[workspace.package]
edition = "2024"
version = "0.1.0"
[workspace.dependencies]
shop-core = { path = "crates/core" }
[workspace.lints.rust]
unsafe_code = "forbid"
"#,
);
write(
&root,
"crates/core/Cargo.toml",
"[package]\nname = \"shop-core\"\nversion.workspace = true\nedition.workspace = true\n\n[lints]\nworkspace = true\n",
);
let body = if with_unsafe {
"pub fn price() -> u32 { unsafe { std::hint::unreachable_unchecked() } }\n"
} else {
"pub fn price() -> u32 { 900 }\n"
};
write(&root, "crates/core/src/lib.rs", body);
write(
&root,
"crates/app/Cargo.toml",
"[package]\nname = \"shop-app\"\nversion.workspace = true\nedition.workspace = true\n\n[dependencies]\nshop-core.workspace = true\n\n[lints]\nworkspace = true\n",
);
write(
&root,
"crates/app/src/main.rs",
"fn main() { println!(\"price={}\", shop_core::price()); }\n",
);
let out = Command::new("cargo")
.args(["run", "--offline", "-q", "-p", "shop-app"])
.current_dir(&root)
.env_remove("CARGO_TARGET_DIR")
.output()
.expect("cargo");
let text =
String::from_utf8_lossy(&out.stdout).into_owned() + &String::from_utf8_lossy(&out.stderr);
(out.status.success(), text, root)
}
fn main() {
let mut ok = true;
let mut check = |cond: bool, what: String| {
println!("[{}] {what}", if cond { "ok" } else { "FAIL" });
ok &= cond;
};
// 1. 같은 크레이트 안에서 쓰기
let item = shop::Item::new("pen", 1000);
check(
item.price() == 900 && item.name == "pen",
"re-export한 shop::Item, private pricing을 거친 가격 900".into(),
);
check(
shop::catalog::count_for_metrics() == 1,
"pub(crate) 함수는 크레이트 안에서 보인다".into(),
);
// 2. 컴파일러가 막는 것
let d = tmp("err");
let lib = ["--crate-type=lib"];
let (_, e) = rustc(
&d,
"a.rs",
&format!("{SHOP}pub fn f() -> u32 {{ shop::pricing::discounted(1) }}"),
&lib,
);
check(
e.contains("E0603") && e.contains("module `pricing` is private"),
"private 모듈의 함수 -> E0603".into(),
);
let (_, e) = rustc(
&d,
"b.rs",
&format!("{SHOP}pub fn f(i: &shop::Item) -> u32 {{ i.price_cents }}"),
&lib,
);
check(e.contains("E0616"), "private 필드 읽기 -> E0616".into());
let (_, e) = rustc(
&d,
"c.rs",
&format!(
"{SHOP}pub fn f() -> shop::Item {{ shop::Item {{ name: String::new(), price_cents: 1 }} }}"
),
&lib,
);
check(
e.contains("E0451"),
"private 필드가 있는 구조체를 바깥에서 리터럴로 만들기 -> E0451".into(),
);
// 3. 크레이트 경계: 같은 코드를 라이브러리로 만들고 다른 크레이트에서 쓴다.
let (built, e) = rustc(
&d,
"shoplib.rs",
SHOP,
&["--crate-type=rlib", "--crate-name=shoplib"],
);
check(built, format!("라이브러리 크레이트 빌드 {}", e.trim()));
let ext = format!("shoplib={}", d.join("libshoplib.rmeta").display());
let (good, e) = rustc(
&d,
"user1.rs",
"pub fn f() -> String { shoplib::shop::Item::new(\"pen\", 1).name }",
&["--crate-type=lib", "--extern", &ext],
);
check(
good,
format!("다른 크레이트에서 pub 경로 사용 OK {}", e.trim()),
);
let (_, e) = rustc(
&d,
"user2.rs",
"pub fn f() -> usize { shoplib::shop::catalog::count_for_metrics() }",
&["--crate-type=lib", "--extern", &ext],
);
check(
e.contains("E0603") && e.contains("function `count_for_metrics` is private"),
"다른 크레이트에서 pub(crate) -> E0603".into(),
);
// 4. 기능 플래그: cargo는 --features json을 --cfg feature="json"으로 넘긴다.
let feat = r#"
pub fn formats() -> Vec<&'static str> {
let mut v = vec!["text"];
#[cfg(feature = "json")]
v.push("json");
#[cfg(feature = "yaml")]
v.push("yaml");
v
}
"#;
let (a, _) = rustc(
&d,
"f1.rs",
feat,
&[
"--crate-type=lib",
"--cfg",
"feature=\"json\"",
"--cfg",
"feature=\"yaml\"",
],
);
check(a, "더하기만 하는 기능 둘은 함께 켜도 컴파일된다".into());
let exclusive = "#[cfg(all(feature = \"sqlite\", feature = \"postgres\"))]\ncompile_error!(\"feature \\\"sqlite\\\" and feature \\\"postgres\\\" cannot be enabled at the same time\");\n";
let (b, e) = rustc(
&d,
"f2.rs",
exclusive,
&[
"--crate-type=lib",
"--cfg",
"feature=\"sqlite\"",
"--cfg",
"feature=\"postgres\"",
],
);
check(
!b && e.contains("cannot be enabled at the same time"),
"서로 배타적인 기능 둘 -> compile_error!".into(),
);
// 5. 워크스페이스
let (built, text, root) = workspace_demo(false);
check(
built && text.contains("price=900"),
format!(
"워크스페이스 빌드·실행: {}",
text.lines().last().unwrap_or("")
),
);
check(
root.join("Cargo.lock").exists(),
"Cargo.lock은 루트에 하나".into(),
);
check(
!root.join("crates/app/Cargo.lock").exists()
&& root.join("target").exists()
&& !root.join("crates/app/target").exists(),
"target/도 루트에 하나".into(),
);
let (built, text, _) = workspace_demo(true);
let line = text
.lines()
.find(|l| l.contains("error"))
.unwrap_or("")
.to_string();
check(
!built && text.contains("usage of an `unsafe` block") && text.contains("-F unsafe-code"),
format!("[workspace.lints] unsafe_code = \"forbid\"가 멤버에 걸린다: {line}"),
);
assert!(ok);
println!("ALL CHECKS PASSED");
}
#[cfg(test)]
mod tests {
use super::shop;
#[test]
fn child_module_test_sees_parent_items() {
assert_eq!(shop::Item::new("cup", 2000).price(), 1800);
}
}