부트스트래핑이 하는 일
참고: 이 문서는 주로 사용자 대상 정보를 다룹니다. 부트스트랩 내부 구조에 대해서는 bootstrap/README.md를 참고하십시오.
부트스트래핑의 단계
개요
- 0단계: 미리 컴파일된 컴파일러와 표준 라이브러리
- 1단계: 이전 컴파일러로 현재 코드를 빌드
- 2단계: 진정한 현재 컴파일러
- 3단계: 동일 결과 테스트
rustc 컴파일은 여러 단계에 걸쳐 이루어집니다. 다음은 RustConf 2022에서 Jynn Nelson이 발표한 부트스트래핑에 관한 강연의 다이어그램을 바탕으로 만든 다이어그램이며, 자세한 설명은 아래에 있습니다.
A, B, C, D는 부트스트래핑 단계의 순서를 나타냅니다. 파란색 노드는 다운로드되고, 노란색 노드는 stage0 컴파일러로 빌드되며, 초록색 노드는 stage1 컴파일러로 빌드됩니다.
graph TD
s0c["stage0 compiler (1.86.0-beta.1)"]:::downloaded -->|A| s0l("stage0 std (1.86.0-beta.1)"):::downloaded;
s0c & s0l --- stepb[ ]:::empty;
stepb -->|B| s0ca["stage0 compiler artifacts (1.87.0-dev)"]:::with-s0c;
s0ca -->|copy| s1c["stage1 compiler (1.87.0-dev)"]:::with-s0c;
s1c -->|C| s1l("stage1 std (1.87.0-dev)"):::with-s1c;
s1c & s1l --- stepd[ ]:::empty;
stepd -->|D| s1ca["stage1 compiler artifacts (1.87.0-dev)"]:::with-s1c;
s1ca -->|copy| s2c["stage2 compiler"]:::with-s1c;
classDef empty width:0px,height:0px;
classDef downloaded fill: lightblue;
classDef with-s0c fill: yellow;
classDef with-s1c fill: lightgreen;
0단계: 미리 컴파일된 컴파일러
stage0 컴파일러는 기본적으로 아주 최근의 베타 rustc 컴파일러와 그에 딸린 동적 라이브러리이며, 부트스트랩이 이를 대신 다운로드해 줍니다. (bootstrap.toml에서 설정하여 stage0을 다른 것으로 바꿀 수도 있습니다.)
그런 다음 미리 컴파일된 stage0 컴파일러는 미리 컴파일된 stage0 std로 src/bootstrap과 compiler/rustc를 컴파일하는 데에만 사용됩니다.
stage1 컴파일러를 빌드할 때는 미리 컴파일된 stage0 컴파일러와 std를 사용한다는 점에 유의하십시오. 따라서 트리에서 새로 빌드한 std를 갖춘 컴파일러를 사용하려면 stage2 컴파일러를 빌드해야 합니다.
여기에는 두 가지 개념이 관련되어 있습니다. 하나는 컴파일러(및 그 의존성 집합)이고, 다른 하나는 그 컴파일러의 ‘타깃’ 또는 ‘오브젝트’ 라이브러리(std와 rustc)입니다. 둘 다 단계적으로 빌드되지만, 서로 엇갈린 방식으로 진행됩니다.
1단계: 이전 컴파일러로 현재 코드를 빌드
그런 다음 rustc 소스 코드를 stage0 컴파일러로 컴파일하여 stage1 컴파일러를 만듭니다.
2단계: 진정한 현재 컴파일러
그런 다음 트리 내 std와 함께 stage1 컴파일러를 사용해 컴파일러를 다시 빌드하여 stage2 컴파일러를 만듭니다.
stage1 컴파일러 자체는 미리 컴파일된 stage0 컴파일러와 std로 빌드되었으므로, 작업 디렉터리에 있는 소스로 빌드된 것이 아닙니다. 즉, stage0 컴파일러가 생성한 ABI는 stage1 컴파일러가 만들었을 ABI와 일치하지 않을 수 있으며, 이로 인해 동적 라이브러리, 테스트, 그리고 rustc_private를 사용하는 도구에서 문제가 생길 수 있습니다.
proc_macro 크레이트는 proc_macro::bridge라는 C FFI 계층을 통해 이 문제를 피하므로 stage1과 함께 사용할 수 있다는 점에 유의하십시오.
stage2 컴파일러는 rustup을 비롯한 다른 모든 설치 방법으로 배포되는 컴파일러입니다. 하지만 먼저 이전 컴파일러로 새 컴파일러를 빌드한 다음, 그 컴파일러로 새 컴파일러 자신을 다시 빌드해야 하므로 빌드하는 데 매우 오랜 시간이 걸립니다.
개발할 때는 보통 --stage 1 플래그만 사용하여 빌드하면 됩니다. 컴파일러 빌드하기를 참고하십시오.
3단계: 동일 결과 테스트
3단계는 선택 사항입니다. 새 컴파일러가 제대로 동작하는지 확인하려면 stage2 컴파일러로 라이브러리를 빌드할 수 있습니다. 무언가 망가지지 않았다면 그 결과는 이전과 동일해야 합니다.
단계 빌드하기
./x 스크립트는 편의를 위해 각 하위 명령마다 사용자가 의도했을 가능성이 가장 높은 단계를 선택하려고 합니다. 다음은 몇 가지 x 명령과 각 명령의 기본 단계입니다:
check:--stage 1clippy:--stage 1doc:--stage 1build:--stage 1test:--stage 1dist:--stage 2install:--stage 2bench:--stage 2
--stage N을 명시적으로 전달하면 언제든지 단계를 재정의할 수 있습니다.
단계에 대한 자세한 내용은 아래를 참고하십시오.
부트스트래핑의 복잡한 점
빌드 시스템은 현재 베타 컴파일러를 사용하여 stage1 부트스트래핑 컴파일러를 빌드하므로, 컴파일러 소스 코드는 일부 기능이 베타에 도달하기 전까지 그 기능을 사용할 수 없습니다(그렇지 않으면 베타 컴파일러가 해당 기능을 지원하지 않기 때문입니다). 반면에 컴파일러 내장 함수와 내부 기능의 경우에는 해당 기능을 반드시 사용해야 합니다. 또한 컴파일러는 nightly 기능(#![feature(...)])을 광범위하게 사용합니다. 이 문제를 어떻게 해결할 수 있습니까?
사용되는 방법은 두 가지입니다:
- 빌드 시스템은
stage0으로 빌드할 때--cfg bootstrap을 설정하므로,cfg(not(bootstrap))를 사용하여stage1로 빌드할 때에만 기능을 사용하도록 할 수 있습니다. 이런 방식으로--cfg bootstrap을 설정하는 것은 막 안정화된 기능, 즉stage0으로 빌드할 때는#![feature(...)]가 필요하지만stage1에서는 필요하지 않은 기능에 사용됩니다. - 빌드 시스템은
RUSTC_BOOTSTRAP=1을 설정합니다. 이 특수 변수는 Rust의 안정성 보장을 깨뜨린다는 것을 의미합니다. 즉,nightly가 아닌 컴파일러에서도#![feature(...)]를 사용할 수 있게 합니다.RUSTC_BOOTSTRAP=1설정은 컴파일러를 부트스트래핑할 때를 제외하고는 절대 사용해서는 안 됩니다.
부트스트랩 단계 이해하기
개요
이 절에서는 개별 부트스트랩 단계를 자세히 살펴봅니다.
./x가 사용하는 규칙은 다음과 같습니다:
--stage N플래그는 N단계 컴파일러(stageN/rustc)를 실행한다는 의미입니다.- “N단계 산출물“은 N단계 컴파일러가 생성한 빌드 산출물입니다.
- 스테이지 N+1 컴파일러는 스테이지 N 산출물로 조립됩니다. 이 과정을 업리프팅(uplifting)이라고 합니다.
빌드 산출물
./x로 빌드할 수 있는 것은 모두 빌드 아티팩트입니다. 빌드 산출물에는 다음이 포함되며, 이에 국한되지 않습니다:
stage0-rustc/rustc-main과 같은 바이너리stage0-sysroot/rustlib/libstd-6fae108520cf72fe.so와 같은 공유 오브젝트stage0-sysroot/rustlib/libstd-6fae108520cf72fe.rlib와 같은 rlib 파일doc/std와 같이 rustdoc이 생성한 HTML 파일
예시
./x test tests/ui는stage1컴파일러를 빌드하고 그 컴파일러에 대해compiletest를 실행한다는 뜻입니다. 컴파일러를 작업하고 있다면 보통 이 테스트 명령을 사용하면 됩니다../x test --stage 0 library/std는rustc를 소스에서 빌드하지 않고 표준 라이브러리에 대해 테스트를 실행한다는 의미입니다(‘stage0으로 빌드한 다음 산출물을 테스트’). 표준 라이브러리를 작업하고 있다면 보통 이 테스트 명령을 사용하면 됩니다../x build --stage 0은 stage0rustc로 빌드한다는 뜻입니다../x doc --stage 1은 stage0rustdoc을 사용해 문서를 생성한다는 뜻입니다.
하면 안 되는 것의 예시
./x test --stage 0 tests/ui는 쓸모가 없습니다. 이 명령은 베타 컴파일러에 대해 테스트를 실행하며rustc를 소스에서 빌드하지 않습니다. 대신stage1을 소스에서 빌드하는test tests/ui를 사용하십시오../x test --stage 0 compiler/rustc는 컴파일러를 빌드하지만 테스트는 전혀 실행하지 않습니다. 이 명령은cargo test -p rustc를 실행하지만,cargo는 Rust의 테스트를 이해하지 못합니다. 이 명령을 사용할 필요는 없을 것입니다. 대신 (인자 없이)test를 사용하십시오../x build --stage 0 compiler/rustc는 컴파일러를 빌드하지만libstd는 물론libcore조차 빌드하지 않습니다. 대부분의 경우에는 대신./x build library를 사용하는 것이 좋으며, 이렇게 하면 언어 항목을 정의할 필요 없이 프로그램을 컴파일할 수 있습니다.
빌드와 실행의 차이
요약하면, 스테이지 0은 stage0 컴파일러를 사용하여 stage0 산출물을 만들고, 이 산출물은 나중에 끌어올려져 stage1 컴파일러가 됩니다.
0을 제외한 각 스테이지에서는 두 가지 주요 단계가 수행됩니다:
std가 스테이지 N 컴파일러로 컴파일됩니다.- 그
std는 스테이지 N 산출물(스테이지 N+1 컴파일러)을 포함하여 스테이지 N 컴파일러로 빌드된 프로그램에 링크됩니다.
스테이지 N 산출물을 스테이지 N 컴파일러로 빌드하는 “또 하나의” 프로그램으로 생각하면 어느 정도 직관적입니다. build --stage N compiler/rustc는 스테이지 N 산출물을 스테이지 N 컴파일러가 빌드한 std에 링크하는 것입니다.
스테이지와 std
여기에는 두 개의 std 라이브러리가 관여한다는 점에 유의하십시오:
- 스테이지 N-1로 빌드되어
stageN/rustc에 링크된 라이브러리(스테이지 N-1std) - stage N에서 빌드된
stageN/rustc로 프로그램을 컴파일하는 데 사용하는 라이브러리(stage Nstd).
stage N 컴파일러로 쓸모 있는 작업을 하려면 stage N std가 사실상 반드시 필요합니다. 이것이 없으면 #![no_core]를 사용하는 프로그램만 컴파일할 수 있는데, 이는 그다지 유용하지 않습니다!
이 둘이 달라야 하는 이유는 반드시 ABI 호환이 보장되지는 않기 때문입니다. nightly에는 베타에 없는 새로운 레이아웃 최적화, MIR 변경, 또는 Rust 메타데이터에 대한 그 밖의 변경이 있을 수 있습니다.
--keep-stage 1 library/std가 쓰이는 곳도 바로 여기입니다. 컴파일러에 대한 변경 대부분은 실제로 ABI를 바꾸지 않으므로, 일단 stage1에서 std를 만들어 두었다면 아마 다른 컴파일러에서도 그대로 재사용할 수 있을 것입니다. ABI가 바뀌지 않았다면 그대로 진행하면 되며, 그 std를 다시 컴파일하는 데 시간을 들일 필요가 없습니다. --keep-stage 플래그는 단순히 빌드 스크립트가 이전 컴파일 결과에 문제가 없다고 가정하도록 지시하며, cargo 호출을 건너뛰고 해당 산출물을 적절한 위치로 복사합니다.
rustc 크로스 컴파일
크로스 컴파일은 다른 아키텍처에서 실행될 코드를 컴파일하는 과정입니다. 예를 들어 x86 머신을 사용해 ARM용 rustc를 빌드하려는 경우가 있을 수 있습니다. 크로스 컴파일할 때는 stage2 std를 빌드하는 방식이 다릅니다.
이는 ./x가 다음과 같은 로직을 사용하기 때문입니다. HOST와 TARGET이 같으면 stage2에 stage1 std를 재사용합니다! 이것이 타당한 이유는 stage1 std가 stage1 컴파일러, 즉 현재 체크아웃한 소스 코드를 사용하는 컴파일러로 컴파일되었기 때문입니다. 따라서 이는 stage2/rustc가 컴파일할 std와 동일해야 합니다(그러므로 ABI 호환도 됩니다).
하지만 크로스 컴파일할 때는 stage1 std가 호스트에서만 실행됩니다. 그래서 stage2 컴파일러가 타깃용 std를 다시 컴파일해야 합니다.
(표에서 stage2가 호스트가 아닌 std 타깃만 빌드하는 것을 확인하십시오.)
’sysroot’란 무엇입니까?
cargo로 프로젝트를 빌드하면 의존성의 빌드 산출물은 보통 target/debug/deps에 저장됩니다. 여기에는 cargo가 알고 있는 의존성만 들어 있으며, 특히 표준 라이브러리는 없습니다. 그렇다면 std나 proc_macro는 어디에서 오는 것입니까? 이들은 컴파일러가 런타임에 빌드 산출물을 불러오는 여러 디렉터리의 루트인 sysroot에서 옵니다. 다만 sysroot에는 표준 라이브러리만 저장되는 것이 아니라, 런타임에 불러와야 하는 모든 것이 포함됩니다. 여기에는 다음이 포함됩니다(단, 이에 국한되지는 않습니다):
libstd/libtest/libproc_macro라이브러리.rustc_private를 사용할 때의 컴파일러 크레이트 자체. 트리 내부에서는 이들이 항상 존재하며, 트리 외부에서는rustup으로rustc-dev를 설치해야 합니다.- LLVM 프로젝트의 공유 오브젝트 파일
libLLVM.so. 트리 내부에서는 이것을 소스에서 빌드하거나 CI에서 내려받으며, 트리 외부에서는rustup으로llvm-tools-preview를 설치해야 합니다.
지금까지 나열한 산출물은 모두 컴파일러의 런타임 의존성입니다. 이들은 rustc --print sysroot로 확인할 수 있습니다:
$ ls $(rustc --print sysroot)/lib
libchalk_derive-0685d79833dc9b2b.so libstd-25c6acf8063a3802.so
libLLVM-11-rust-1.50.0-nightly.so libtest-57470d2aa8f7aa83.so
librustc_driver-4f0cc9f50e53f0ba.so libtracing_attributes-e4be92c35ab2a33b.so
librustc_macros-5f0ec4a119c6ac86.so rustlib
표준 라이브러리에도 런타임 의존성이 있습니다! 이들은 lib/ 바로 아래가 아니라 lib/rustlib/에 있습니다.
$ ls $(rustc --print sysroot)/lib/rustlib/x86_64-unknown-linux-gnu/lib | head -n 5
libaddr2line-6c8e02b8fedc1e5f.rlib
libadler-9ef2480568df55af.rlib
liballoc-9c4002b5f79ba0e1.rlib
libcfg_if-512eb53291f6de7e.rlib
libcompiler_builtins-ef2408da76957905.rlib
lib/rustlib/ 디렉터리에는 hashbrown, cfg_if 같은 라이브러리가 들어 있습니다. 이들은 표준 라이브러리의 공개 API에 속하지 않지만 표준 라이브러리를 구현하는 데 사용됩니다. 또한 lib/rustlib/는 링커의 검색 경로에 포함되지만, lib는 절대 검색 경로에 포함되지 않습니다.
-Z force-unstable-if-unmarked
lib/rustlib/는 검색 경로에 포함되므로 여기에 어떤 크레이트를 넣을지 신중해야 합니다. 특히 표준 라이브러리를 제외한 모든 크레이트는 -Z force-unstable-if-unmarked 플래그로 빌드됩니다. 따라서 이들을 불러오려면 #![feature(rustc_private)]를 사용해야 합니다(항상 사용할 수 있는 표준 라이브러리와는 다릅니다).
-Z force-unstable-if-unmarked 플래그는 올바른 크레이트가 unstable로 표시되도록 강제하는 데 도움이 되는 여러 목적을 가지고 있습니다. 이 플래그는 주로 rustc와 표준 라이브러리가 그 자체로는 staged_api를 사용하지 않는 crates.io의 임의의 크레이트에 링크할 수 있도록 하기 위해 도입되었습니다. rustc 또한 이 플래그를 이용해 자신의 모든 크레이트를 rustc_private 기능과 함께 unstable로 표시하므로, 각 크레이트를 일일이 unstable로 표시하지 않아도 됩니다.
이 플래그는 부트스트랩 스크립트에 의해 rustc 전체와 표준 라이브러리에 자동으로 적용됩니다. 컴파일러와 그 모든 의존성이 sysroot에 담겨 모든 사용자에게 배포되기 때문에 이것이 필요합니다.
이 플래그의 효과는 다음과 같습니다.
- 크레이트 자체가
stable또는unstable로 표시되어 있지 않으면, 해당 크레이트를rustc_private기능과 함께 “unstable“로 표시합니다. - 이 크레이트들이 속성 없이도 강제로 불안정하게 지정된 다른 크레이트에 접근할 수 있게 합니다. 일반적으로 크레이트가 다른
unstable크레이트를 사용하려면#![feature(rustc_private)]속성이 필요합니다. 그러나 그렇게 하면 crates.io의 크레이트는feature(rustc_private)속성을 갖지 않으므로 자신의 의존성에 접근할 수 없게 됩니다. 하지만 모든 것이-Z force-unstable-if-unmarked로 컴파일됩니다.
-Z force-unstable-if-unmarked를 사용하지 않는 코드가 강제로 불안정하게 지정된 이 크레이트들에 접근하려면 #![feature(rustc_private)] 크레이트 속성을 포함해야 합니다. 이는 Miri나 clippy처럼 rustc 자체를 링크하는 도구에 필요합니다.
sysroot에 관한 더 많은 논의는 다음에서 찾아볼 수 있습니다.
sysroot에서 불러오는 의존성에extern crate를 사용하는 이유를 설명하는 rustdoc PR- Zulip에서의 sysroot에 관한 논의
- rustdoc를 트리 외부에서 빌드하는 것에 관한 논의
bootstrap이 호출하는 명령에 플래그 전달하기
편리하게도 ./x를 사용하면 부트스트래핑할 때 rustc와 cargo에 스테이지별 플래그를 전달할 수 있습니다. RUSTFLAGS_BOOTSTRAP 환경 변수는 부트스트랩 스테이지(stage0)에 RUSTFLAGS로 전달되며, RUSTFLAGS_NOT_BOOTSTRAP은 이후 스테이지의 산출물을 빌드할 때 전달됩니다. RUSTFLAGS도 동작하지만 bootstrap 자체의 빌드에도 영향을 미치므로, 이를 사용하고 싶은 경우는 드물 것입니다. 마지막으로 MAGIC_EXTRA_RUSTFLAGS는 cargo 캐시를 우회하여 모든 의존성을 다시 컴파일하지 않고도 rustc에 플래그를 전달합니다.
RUSTDOCFLAGS,RUSTDOCFLAGS_BOOTSTRAP,RUSTDOCFLAGS_NOT_BOOTSTRAP은RUSTFLAGS와 비슷하지만rustdoc용입니다.CARGOFLAGS는 cargo 자체에 인자를 전달합니다.CARGOFLAGS_BOOTSTRAP과CARGOFLAGS_NOT_BOOTSTRAP은RUSTFLAGS_BOOTSTRAP과 비슷하게 동작합니다.--test-args는 테스트 러너에 인자를 그대로 전달합니다.tests/ui의 경우 테스트 러너는compiletest입니다. 단위 테스트와 문서 테스트의 경우 테스트 러너는libtest러너입니다.
대부분의 테스트 러너는 --help를 지원하며, 이를 사용해 해당 러너가 받는 옵션을 확인할 수 있습니다.
환경 변수
부트스트래핑 중에는 컴파일러 내부에서 사용하는 환경 변수가 많이 쓰입니다. rustc의 중간 버전을 실행하려는 경우, 이러한 환경 변수 중 일부를 직접 설정해야 할 때가 있습니다. 그렇지 않으면 다음과 같은 오류가 발생합니다.
thread 'main' panicked at 'RUSTC_STAGE was not set: NotPresent', library/core/src/result.rs:1165:5
./stageN/bin/rustc가 환경 변수에 관한 오류를 낸다면, 이는 보통 무언가가 크게 잘못되었다는 뜻입니다. 예를 들어 rustc나 std, 또는 환경 변수에 의존하는 무언가를 컴파일하려는 경우가 이에 해당합니다. 드물게 이런 상황에서 실제로 rustc를 호출해야 한다면, x 명령에 -vvv를 추가하여 부트스트랩 심(shim)이 모든 env 변수를 출력하도록 할 수 있습니다.
마지막으로, 부트스트랩은 cc-rs 크레이트를 사용하며, 이 크레이트는 환경 변수를 통해 C 컴파일러와 C 플래그를 설정하는 자체 방식을 갖추고 있습니다.
빌드 명령의 stdout 설명
이 부분에서는 빌드 명령의 stdout이 실제로 어떻게 나타나는지 살펴보겠습니다(위 주제와 비슷하지만 더 자세하고 완전한 문서입니다). x build --dry-run 명령을 실행하면 빌드 출력은 다음과 비슷하게 나타납니다.
Building stage0 library artifacts (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu)
Copying stage0 library from stage0 (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu / x86_64-unknown-linux-gnu)
Building stage0 compiler artifacts (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu)
Copying stage0 rustc from stage0 (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu / x86_64-unknown-linux-gnu)
Assembling stage1 compiler (x86_64-unknown-linux-gnu)
Building stage1 library artifacts (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu)
Copying stage1 library from stage1 (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu / x86_64-unknown-linux-gnu)
Building stage1 tool rust-analyzer-proc-macro-srv (x86_64-unknown-linux-gnu)
Building rustdoc for stage1 (x86_64-unknown-linux-gnu)
stage0 {std,컴파일러} 산출물 빌드
이 단계들은 제공된(보통은 다운로드된) 컴파일러를 사용하여 로컬 Rust 소스를 우리가 사용할 수 있는 라이브러리로 컴파일합니다.
stage0 {std,rustc} 복사
이 단계는 라이브러리와 컴파일러 산출물을 cargo에서 stage0-sysroot/lib/rustlib/{target-triple}/lib로 복사합니다
stage1 컴파일러 조립
이 단계는 “stage0 … 산출물 빌드“에서 빌드한 라이브러리를 stage1 컴파일러의 lib/ 디렉터리로 복사합니다. 이들은 컴파일러 자체가 실행되는 데 사용하는 호스트 라이브러리입니다. 이들은 새 컴파일러가 생성하는 산출물에서는 실제로 사용되지 않습니다. 이 단계는 또한 우리가 생성한 rustc와 rustdoc 바이너리를 build/$HOST/stage/bin으로 복사합니다.
stage1/bin/rustc는 stage0(사전 컴파일된) 컴파일러와 std로 빌드된, 완전히 동작하는 컴파일러입니다. 트리 내(in-tree) 컴파일러와 std를 사용해 전부 소스에서 빌드된 컴파일러를 사용하려면 stage2 컴파일러를 빌드해야 하며, 이 컴파일러는 stage1(트리 내) 컴파일러와 std를 사용하여 컴파일됩니다.