rust-lang/cargo ソースコードリーディング

はじめに

cargoにはバイナリの名前をパッケージ名と異なるものにする機能がある。 この機能はcargo-feature different_binary_nameという。

Cargo.tomlに以下を追記すると、cargo buildで出力されるバイナリはpiyoになる。

[[bin]]
name = "piyo"
path = "src/piyo.rs"

この機能は以下のIssueで提案され採用された。

基本事項

この記事はこのPRをもとにrust-lang/cargoのソースコードを読んでわかったことをまとめたものである。もともと自分用メモだったので、汚いのはご容赦ください、

まず、ファイル名の出力に関しての基本事項を以下に示した。これはコードを読み進めてわかったことだが、本記事ではこれらをもとにソースコードを見ていく。

  • Target構造体: 全体のconfig情報を管理する
  • OutputFile構造体: 出力ファイルごとに作られる構造体
    • 出力先パスの情報
    • ハードリンクに関する情報

PRで提案された手法

  • Cargo.tomlからバイナリ名を取得
  • バイナリ名が指定されていない => 従来通り、パッケージ名を使う。
  • バイナリ名指定あり => Target構造体にそのバイナリ名を持たせる

ハードリンクに関して

cargo buildすると、バイナリはtarget/debugに作られるが、バイナリの実体はそれより下の階層(target/debug/depsなど)にある。 targetの階層につくられるバイナリは実はハードリンクである。

実際に読む

Targetはバイナリやライブラリの情報を一括してもつ。 Target.bin_namedifferent_binary_nameを実現するためにあるフィールド。

// src/cargo/core/manifest.rs

// Information about a binary, a library, an example, etc. that is
// part of the package.
pub struct Target {
    inner: Arc<TargetInner>,
}

struct TargetInner {
    kind: TargetKind,
    name: String,
    // Note that `bin_name` is used for the cargo-feature `different_binary_name`
    // Some(..)ならば、[[bin]]でバイナリ名が指定されている
    // Noneならば、バイナリ名が指定されていない
    bin_name: Option<String>,
    // Note that the `src_path` here is excluded from the `Hash` implementation
    // as it's absolute currently and is otherwise a little too brittle for
    // causing rebuilds. Instead the hash for the path that we send to the
    // compiler is handled elsewhere.
    src_path: TargetSourcePath,
    required_features: Option<Vec<String>>,
    tested: bool,
    benched: bool,
    doc: bool,
    doctest: bool,
    harness: bool, // whether to use the test harness (--test)
    for_host: bool,
    proc_macro: bool,
    edition: Edition,
}

pub enum TargetKind {
    Lib(Vec<CrateType>),
    Bin,
    Test,
    Bench,
    ExampleLib(Vec<CrateType>),
    ExampleBin,
    CustomBuild,
}

pub fn bin_target(
        name: &str,
        bin_name: Option<String>,
        src_path: PathBuf,
        required_features: Option<Vec<String>>,
        edition: Edition,
) -> Target {
    ...
}

impl Target {
    ...
    // バイナリの名前を返すメソッド
    pub fn binary_filename(&self) -> Option<String> {
        self.inner.bin_name.clone()
    }
    
    // バイナリの名前をセットするメソッド
    pub fn set_binary_name(&mut self, bin_name: Option<String>) -> &mut Target {
        Arc::make_mut(&mut self.inner).bin_name = bin_name;
        self
    }
    ...
}

以下は関連するコード tomlで渡されたfilenameをTarget構造体に渡す。

// src/cargo/util/toml/targets.rs

fn clean_bins(
    features: &Features,
    toml_bins: Option<&Vec<TomlBinTarget>>,
    package_root: &Path,
    package_name: &str,
    edition: Edition,
    autodiscover: Option<bool>,
    warnings: &mut Vec<String>,
    errors: &mut Vec<String>,
    has_lib: bool,
) -> CargoResult<Vec<Target>> {
    ...
    let mut target = Target::bin_target(
            &bin.name(),
            bin.filename.clone(),
            path,
            bin.required_features.clone(),
            edition,
        );
    ...
}

出力するバイナリの名前を決定する関数。 重要なのはSomeとNoneの場合分け。

// src/cargo/core/compiler/build_context/target_info.rs

/// The filename for this FileType that Cargo should use when "uplifting"
/// it to the destination directory.
pub fn uplift_filename(&self, target: &Target) -> String {
    let name = match target.binary_filename() {
        // [[bin]]でバイナリの名前が指定されているのでそれを使う
        Some(name) => name,
        // パッケージ名をバイナリの名前に使う
        None => {
            // For binary crate type, `should_replace_hyphens` will always be false.
            // 名前に含まれるハイフンを処理する
            if self.should_replace_hyphens {
                target.crate_name()
            } else {
                target.name().to_string()
            }
        }
    };
    format!("{}{}{}", self.prefix, name, self.suffix)
}

次にハードリンクについて。 本PRでコードの修正は入ってないが、参考のために載せておく。

uplift_to関数: Option<PathBuff>型を返す 戻り値はSome(path)なら、pathにハードリンクを作る、Noneなら、ハードリンクを作らないことを意味する。

calc_outputs_rustc関数: CargoResult<Vec<OutputFile>>型を返す。 戻り値はrustcが必要とする出力ファイルの情報をもつ。

// src/cargo/core/compiler/context/compilation_files.rs

impl<'a, 'cfg: 'a> CompilationFiles<'a, 'cfg> {
    ...
    /// Returns the path where the output for the given unit and FileType
    /// should be uplifted to.
    ///
    /// Returns `None` if the unit shouldn't be uplifted (for example, a
    /// dependent rlib).
    fn uplift_to(&self, unit: &Unit, file_type: &FileType, from_path: &Path) -> Option<PathBuf> {
        ...
    }

    // rustcによって作成されたファイル情報を出力する
    /// Computes the actual, full pathnames for all the files generated by rustc.
    ///
    /// The `OutputFile` also contains the paths where those files should be
    /// "uplifted" to.
    fn calc_outputs_rustc(
        &self,
        unit: &Unit,
        bcx: &BuildContext<'a, 'cfg>,
    ) -> CargoResult<Vec<OutputFile>> {
        ...
        // Convert FileType to OutputFile.
        let mut outputs = Vec::new();
        for file_type in file_types {
            let meta = &self.metas[unit];
            let meta_opt = meta.use_extra_filename.then(|| meta.meta_hash.to_string());
            let path = out_dir.join(file_type.output_filename(&unit.target, meta_opt.as_deref()));

            // ハードリンクで参照する名前を取得
            // If, the `different_binary_name` feature is enabled, the name of the hardlink will
            // be the name of the binary provided by the user in `Cargo.toml`.
            let hardlink = self.uplift_to(unit, &file_type, &path);
            let export_path = if unit.target.is_custom_build() {
                None
            } else {
                self.export_dir.as_ref().and_then(|export_dir| {
                    hardlink
                        .as_ref()
                        .map(|hardlink| export_dir.join(hardlink.file_name().unwrap()))
                })
            };
            outputs.push(OutputFile {
                path,
                hardlink,
                export_path,
                flavor: file_type.flavor,
            });
        }
        Ok(outputs)
    }
    ...
}

cargoが最終的に生成するOutputFile構造体はこんな感じで定義されている。

// src/cargo/core/compiler/context/compilation_files.rs

pub struct OutputFile {
    /// Absolute path to the file that will be produced by the build process.
    pub path: PathBuf,
    /// If it should be linked into `target`, and what it should be called
    /// (e.g., without metadata).
    pub hardlink: Option<PathBuf>,
    /// If `--out-dir` is specified, the absolute path to the exported file.
    pub export_path: Option<PathBuf>,
    /// Type of the file (library / debug symbol / else).
    pub flavor: FileFlavor,
}

感想

PRを読むのは勉強になる。 インクリメンタルコンパイルのためのfingerprintという機能があるらしいが、それがどのようなものか、どのようにcargoで処理されているか気になった。

Ref