Rust 开发命令行工具(中)

2023年 9月 21日 60.2k 0

生活在不可避免地走向庸俗。--王小波

大家好,我是柒八九。

前言

在上一篇Rust 开发命令行工具(上)中我们从项目配置/参数获取/解析文件内容/处理错误信息/信息输出处理等方面。一步一步写出来可以构建出在本地,兼容错误提示,并且有很好的输出形式的本地搜索工具。

以防大家遗忘,我们把最终的代码贴到下面。

use anyhow::{Context, Result};
use clap::Parser;
use indicatif::ProgressBar;
use std::fs::File;
use std::io::{self, BufRead, Write};
use std::path::PathBuf;
use std::thread;
use std::time::Duration;


#[derive(Parser)]
struct Cli {
    /// 要查找的模式
    pattern: String,
    /// 要读取的文件的路径
    path: PathBuf,
}

fn main() -> Result {
    let args = Cli::parse();

    // 打开文件并创建一个 BufReader 来逐行读取
    let file = File::open(&args.path).with_context(|| format!("无法打开文件 {:?}", &args.path))?;
    let reader = io::BufReader::new(file);

    let stdout = io::stdout();
    let stdout_lock = stdout.lock();
    let mut handle = io::BufWriter::new(stdout_lock);
    let pb = ProgressBar::new(100);
    for line in reader.lines() {
        do_hard_work();
        pb.println(format!("[+] 查找到了 #{:?}项", line));
        pb.inc(1);
        let line = line.with_context(|| "无法读取行")?;
        if line.contains(&args.pattern) {
            writeln!(handle, "{}", line)?;
        }
    }

    Ok(())
}

fn do_hard_work() {
    thread::sleep(Duration::from_millis(250));
}

但是,作为一个功能完备的项目,我们还需要做单元测试/集成测试和打包发布。所以,今天我们就从这两面来继续完善我们的Rust项目。

你能所学到的知识点

  • 前置知识点
  • 代码测试
  • 打包并发布 Rust 项目
  • 好了,天不早了,干点正事哇。

    前置知识点

    前置知识点,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略

    同时,由于阅读我文章的群体有很多,所以有些知识点可能我视之若珍宝,尔视只如草芥,弃之如敝履。以下知识点,请酌情使用。

    单元测试 VS 黑盒测试

    单元测试和黑盒测试(也叫集成测试)是两种不同的软件测试方法,它们旨在检查和验证软件的质量和功能,但它们的关注点、方法和目标有所不同。

    单元测试(Unit Testing)

  • 焦点:单元测试关注测试软件的最小功能单元,通常是一个函数、方法或模块。它的目标是验证这个功能单元是否按照预期工作,而不考虑其他组件。

  • 测试者:通常由开发人员编写和执行。开发人员编写测试用例,用于检查函数、方法或模块的各种输入和边界条件。

  • 可见性:单元测试通常具有对代码的白盒访问权限,测试者可以访问和检查被测试单元的内部实现细节,以编写更精确的测试用例。

  • 目标:主要目标是验证单元的正确性,确保它们按照规范执行,并处理各种输入情况。

  • 黑盒测试(Black Box Testing)

  • 焦点:黑盒测试关注测试整个软件系统的功能,而不考虑内部实现。它的目标是验证系统是否按照规范的需求和功能规范工作。

  • 测试者:可以由测试工程师或独立的测试团队执行。测试者不需要了解系统的内部实现,只需关注系统的输入和输出。

  • 可见性:黑盒测试没有对系统的内部实现细节的了解。测试者只能访问系统的外部接口和功能。

  • 目标:主要目标是验证系统是否满足其规范和需求,以及是否在各种输入和条件下表现正常。

  • 在实际项目中,通常需要同时进行单元测试黑盒测试,以确保软件在各个层面上都具有高质量和可靠性。

    Rust trait

    Rust中,trait 是一种特殊的类型,它定义了某些类型的共享行为。trait 提供了一种方式来抽象和共享方法,类似于其他编程语言中的接口。通过实现trait,你可以为自定义类型定义通用的行为,使其能够与其他类型一起工作,从而提高了Rust代码的可复用性和灵活性。

    下面我们简单解释一下trait的使用

  • 定义trait

    我们可以使用trait关键字来定义一个trait,然后在其中声明方法签名。

    trait Printable {
        fn print(&self);
    }
    

    这个示例定义了一个名为Printabletrait,它要求实现该trait的类型必须包含一个名为print的方法。

  • 实现trait

    要使类型实现一个trait,我们需要在类型的定义中使用impl块来实现trait中声明的方法。

    struct MyStruct {
        data: i32,
    }
    
    impl Printable for MyStruct {
        fn print(&self) {
            println!("Data: {}", self.data);
        }
    }
    

    在这个示例中,MyStruct类型实现了Printable trait,提供了print方法的具体实现。

  • 使用trait

    一旦你实现了一个trait,我们可以在任何实现了该trait的类型上调用trait中定义的方法。例如:

    let my_instance = MyStruct { data: 42 };
    my_instance.print();
    

    在这里,我们创建了一个MyStruct的实例并调用了print方法。

  • 总的来说,traitRust中用于实现抽象和共享行为的强大工具,它有助于编写可复用的代码,同时确保类型的安全性和一致性。通过合理使用trait,我们可以编写更清晰、更灵活和更可维护的Rust代码。

    更详细的内容,可以参考我们之前写的Rust 泛型、trait 与生命周期

    Rust的模块系统

    Rust的{模块系统|the module system},包括:

    • 包(Packages): Cargo 的一个功能,它允许你构建、测试和分享 crate
    • Crates :一个模块的树形结构,它形成了库或二进制项目。
    • 模块(Modules)和 use: 允许你控制作用域和路径的私有性。
    • 路径(path):一个命名例如结构体、函数或模块等项的方式

    包和 crate

    • 包(package) 是**提供一系列功能的一个或者多个 crate。**一个包会包含有一个 Cargo.toml 文件,阐述如何去构建这些 crate
    • crate 是一个二进制项或者库。crate root 是一个源文件,Rust 编译器以它为起始点,并构成你的 crate 的根模块。

    包中所包含的内容由几条规则来确立。

  • 一个包中至多只能包含一个{库 crate|library crate};
  • 包中可以包含任意多个{二进制 crate|binary crate};
  • 包中至少包含一个 crate,无论是库的还是二进制的。
  • 输入命令 cargo new xxx:当我们输入了这条命令,Cargo 会给我们的创建一个 Cargo.toml 文件。查看 Cargo.toml 的内容,会发现并没有提到 src/main.rs,因为 Cargo 遵循的一个约定:

    • src/main.rs 就是一个与包同名的{二进制 crate|binary crate} 的 crate 根。
    • 同样的,Cargo 知道如果包目录中包含 src/lib.rs,则包带有与其同名的{库 crate|library crate},且 src/lib.rscrate 根。

    crate 根文件将由 Cargo 传递给 rustc 来实际构建库或者二进制项目。

    如果一个包同时含有 src/main.rssrc/lib.rs,则它有两个 crate:一个库和一个二进制项,且名字都与包相同。

    通过将文件放在 src/bin 目录下,一个包可以拥有多个二进制 crate:每个 src/bin 下的文件都会被编译成一个独立的{二进制 crate|binary crate}。

    一个 crate 会将一个作用域内的相关功能分组到一起,使得该功能可以很方便地在多个项目之间共享

    关于这块的内容,可以参考之前我们写的Rust之包、Crate和模块

    crates.io是个啥?

    crates.ioRust 编程语言社区的官方包管理和分发平台。它类似于其他编程语言中的包管理器,如 PythonPyPIJavaScriptnpm,用于帮助 Rust 开发者分享、发布和获取 Rust 代码库(也称为 "crates")。

    以下是 crates.io 的一些关键特点和功能:

  • 包管理器:crates.io 提供了一个中央存储库,用于托管 Rust crates。开发者可以使用 cargoRust 的包管理工具,轻松地下载、安装和管理这些 crates

  • 包发布:任何 Rust 开发者都可以将自己的 Rust 代码库发布到 crates.io 上,供其他人使用。这使得代码共享和开源社区合作更加容易。

  • 版本控制:每个 crate 都有自己的版本号,允许开发者指定使用特定版本的 crate。这有助于确保代码的稳定性和可靠性。

  • 依赖管理:crates.io 允许 crate 之间建立依赖关系,开发者可以在自己的项目中引入其他 crates 作为依赖项,从而快速构建功能强大的应用程序。

  • 搜索和浏览:crates.io 提供了一个易于使用的网站,允许开发者搜索、浏览和查找他们需要的 Rust crates。网站还提供了有关每个 crate 的详细信息、文档和示例代码。

  • 社区驱动:crates.io 是由 Rust 社区维护和支持的,任何人都可以为平台的发展和改进做出贡献。

  • 总之,crates.ioRust 生态系统的核心组成部分,它使 Rust 开发更加便捷,促进了 Rust 社区的增长和分享代码的文化。开发者可以在上面找到各种各样的 Rust crates,以加速他们的项目开发。

    2. 代码测试

    为了确保我们的程序按照我们的期望工作,最明智的做法是对其进行测试。

    一种简单的方法是编写一个README文件,描述我们的程序应该执行的操作。当我们准备发布新版本时,通过README可以描述我们程序的功能和行为。与此同时,我们还可以通过写下程序应该如何应对错误输入来让我们的程序变的更加严谨。

    自动化测试

    Rust中,#[test] 是一个属性(attribute),用于标记测试函数。Rust内置了一个测试框架,可以使用这个属性来定义和运行测试。

    以下是使用 #[test] 的基本步骤:

  • 首先,确保我们的Rust项目是一个可测试的项目。通常,Rust项目的测试代码存放在一个名为 tests 的目录中,或者在我们的代码中使用条件编译来区分测试代码和生产代码。它允许构建系统发现这些函数并将其作为测试运行,验证它们不会触发panic

  • 创建一个测试函数并标记为 #[test]。测试函数必须返回 ()(unit类型),并且通常不带参数。

    #[test]
    fn test_example() {
        // 在这里编写测试代码
    }
    
  • 在测试函数中编写测试代码,包括调用我们要测试的函数,并使用断言来检查函数的输出是否与预期值匹配。我们可以使用标准库中的 assert! 宏或其他测试断言宏来进行断言。

    #[test]
    fn test_addition() {
        assert_eq!(2 + 2, 4);
    }
    
    #[test]
    fn test_subtraction() {
        assert!(5 - 3 > 0);
    }
    
  • 运行测试。可以使用 Rust 的测试运行器工具来执行测试。常见的测试命令是 cargo test,它会自动查找和运行项目中的所有测试函数。在项目根目录下运行以下命令:

    cargo test
    
  • 测试运行结果会显示在终端中。成功的测试将显示为ok,失败的测试将显示为 fail,并提供失败的详细信息,包括测试函数的名称和失败的断言。我们可以根据这些信息来调试和修复代码。

  • 如果需要更详细的输出,可以使用 --verbose 标志运行测试

    cargo test --verbose
    
  • 我们应该最终得到类似以下的输出:

    通过#[test]我们可以测试我们想测试的核心代码,但是,作为一个CLI通常不仅仅是一个函数,它需要更多的人机交互,例如需要处理用户输入、读取文件和编写输出等,我们不可预知的参数和行为。

    单元测试

    有两种互补的方法来测试功能:

  • 单元测试(unit tests):测试构建完整应用程序的小单元。
  • 黑盒测试(black box tests)或集成测试(integration tests):测试最终应用程序的“外部”。
  • 让我们先从单元测试开始。

    决定去远方,需要一个目的地,我们想要测试哪些东西,我们就需要知道我们的程序功能是啥!总的来说,f789应该打印出与给定模式匹配的行。因此,让我们为这个编写单元测试:我们希望确保我们最重要的逻辑部分有效,并且我们希望以不依赖于我们周围的任何设置代码(例如处理CLI参数等)的方式来执行此操作。

    回到我们的f789的第一个实现,我们在main函数中添加了这个代码块:

    // ...
    for line in content.lines() {
        if line.contains(&args.pattern) {
            println!("{}", line);
        }
    }
    

    上面的代码是不容易进行单元测试的。首先,它在main函数中,因此我们不能轻松地调用它。所以,我们需要将它移出main函数,将这段代码移入一个函数中:

    fn find_matches(content: &str, pattern: &str) {
        for line in content.lines() {
            if line.contains(pattern) {
                println!("{}", line);
            }
        }
    }
    

    现在我们可以在测试中调用这个函数,查看它的输出是什么:

    #[test]
    fn find_a_match() {
        find_matches("frontn789", "789");
        assert_eq!( // 省略了部分代码
    
    

    目前,find_matches通过stdout将内容直接打印到了终端。我们并不能轻松地在测试中捕获这个信息,并且它是不可调试的。

    我们需要以某种方式捕获输出。幸运的是:Rust的标准库提供了一些处理I/O的方式,我们可以使用其中一个称为std::io::Writetrait,它可用于我们可以写入的东西,包括字符串,还有stdout

    有了Wirte的加持,让我们更改我们的函数以接受第三个参数。它应该是实现了Write的任何类型。这样,我们就可以在测试中提供一个简单的字符串,并对其进行断言。以下是我们编写的改良版的find_matches版本:

    fn find_matches(content: &str, pattern: &str, mut writer: impl std::io::Write) {
        for line in content.lines() {
            if line.contains(pattern) {
                writeln!(writer, "{}", line);
            }
        }
    }
    

    新参数是mut writer,也就是说writer是可变(mutable)的。它的类型是impl std::io::Write,我们可以将其解读为实现了Write trait的任何类型的占位符。还要注意,我们用writeln!(writer, …)替换了之前使用的println!(…)println!writeln!的工作方式相同,但始终使用标准输出。

    现在我们可以测试输出:

    #[test]
    fn find_a_match() {
        let mut result = Vec::new();
        find_matches("frontn789", "789", &mut result);
        assert_eq!(result, b"789n");
    }
    

    要在我们的应用程序代码中使用它,我们必须更改main中对find_matches的调用,通过将&mut std::io::stdout()作为第三个参数添加。

    fn main() -> Result {
        let args = Cli::parse();
        let content = std::fs::read_to_string(&args.path)
            .with_context(|| format!("无法读取文件 `{}`", args.path.display()))?;
    
        find_matches(&content, &args.pattern, &mut std::io::stdout());
    
        Ok(())
    }
    

    注意:由于stdout需要字节(而不是字符串),我们使用std::io::Write而不是std::fmt::Write。因此,在我们的测试中,我们给出一个空向量(vector)作为writer(其类型将被推断为Vec),在assert_eq!中,我们使用b"foo"。(b前缀将其转换为字节字符串文字,因此其类型将为&[u8],而不是&str)。

    我们来看最终被改造后的代码。

    use anyhow::{Context, Result};
    use clap::Parser;
    use std::path::PathBuf;
    #[derive(Parser)]
    struct Cli {
        /// 要查找的模式
        pattern: String,
        /// 要读取的文件的路径
        path: PathBuf,
    }
    fn main() -> Result {
        let args = Cli::parse();
        let content = std::fs::read_to_string(&args.path)
            .with_context(|| format!("无法读取文件`{}`", args.path.display()))?;
    
        find_matches(&content, &args.pattern, &mut std::io::stdout());
    
        Ok(())
    }
    
    fn find_matches(content: &str, pattern: &str, mut writer: impl std::io::Write) {
        #[allow(unused_must_use)]
        for line in content.lines() {
            if line.contains(pattern) {
                writeln!(writer, "{}", line);
            }
        }
    }
    #[test]
    fn find_a_match() {
        let mut result = Vec::new();
        find_matches("frontn789", "789", &mut result);
        assert_eq!(result, b"789n");
    }
    
    

    使用cargo test运行上面的代码,运行结果如下:

    将代码拆分为库(library)和二进制(binary)

    到目前为止,我们把所有代码都放在了src/main.rs文件中。这意味着我们当前的项目生成一个单独的二进制文件。但是我们也可以将我们的代码作为一个库提供,方法如下:

  • find_matches函数放入一个新的src/lib.rs文件中。
  • fn前面加上pub(这样它就是pub fn find_matches),以使其成为我们库的用户可以访问的内容。
  • src/main.rs中删除find_matches
  • fn main中,在调用find_matches之前加上f789::,这样它现在是f789::find_matches(…)。这意味着它使用了我们刚刚编写的库中的函数!
  • 我们可以在之前使用find_matches地方做一个改造,并且功能也不会受影响。

    fn main() -> Result {
        // ....
        f789::find_matches(&content, &args.pattern, &mut std::io::stdout());
        //....
    }
    #[test]
    fn find_a_match() {
        //....
        f789::find_matches("frontn789", "789", &mut result);
        // ...
    }
    

    黑盒测试

    到目前为止,我们测试的主要发力点都是业务逻辑层面,这业务逻辑主要集中在find_matches函数中。

    然而,有很多代码我们没有测试:也就是我们需要对外界(人机交互)部分做测试处理。想象一下,如果我们编写了main函数,但是意外地留下了一个硬编码的字符串,而不是使用用户提供的路径参数,会发生什么情况。我们也应该为这些写测试!(这种级别的测试通常称为黑盒测试或系统测试)。

    从本质上讲,我们仍然是在编写函数并使用#[test]进行注释。但是,我们会把这些测试代码放置到新目录中:tests/cli.rs。(按照约定,cargo将在tests/目录中查找集成测试)

    回顾一下,f789是一个在文件中搜索字符串的小工具。我们已经测试了我们可以找到一个匹配项。让我们思考一下我们还可以测试的其他功能。

    • 文件不存在时会发生什么?
    • 当没有匹配项时输出是什么?
    • 当我们忘记一个(或两个)参数时,我们的程序是否会以错误退出?

    这些都是有效的测试用例。

    为了使这些测试更容易进行,我们将使用assert_cmd crate。它有许多很好的辅助功能,允许我们运行我们的二进制文件并查看它的行为。此外,我们还将添加predicates crate,它可以帮助我们编写断言,assert_cmd可以对其进行测试(并且具有出色的错误消息)。我们将这些依赖项添加到Cargo.tomldev dependencies部分,而不是主列表中。它们只在开发crate时需要,而在使用crate时不需要。

    [dev-dependencies]
    assert_cmd = "2.0.12"
    predicates = "3.0.3"
    

    我们直接进入并创建我们的tests/cli.rs文件:

    下面,我们直接用代码注释来说明核心代码的功能

    // 这个crate提供了在运行命令时添加方法的功能,通常用于编写命令行应用程序的测试。
    use assert_cmd::prelude::*;
    // 这个crate提供了编写断言(assertions)的功能,可以用来验证测试的预期结果。
    use predicates::prelude::*;
    // 这是Rust标准库中的模块,它允许你运行外部程序并与之交互。这通常用于测试执行外部命令时的行为。
    use std::process::Command;
    
    #[test]
    fn file_doesnt_exist() -> Result {
        // 这行代码创建了一个 Command 对象,它用于执行一个外部命令行程序。
        // cargo_bin 方法用于查找并返回通过 Cargo 构建的可执行文件。
        // 在这里,它尝试查找名为 "f789" 的可执行文件。
        let mut cmd = Command::cargo_bin("f789")?;
        // 这两行代码向命令添加了两个参数。
        // 它们模拟了在命令行中运行 "f789 front text.txt" 命令。
        cmd.arg("front").arg("text.txt");
        cmd.assert()
            .failure()
            .stderr(predicate::str::contains("无法读取文件"));
    
        Ok(())
    }
    
    
    

    我们可以使用cargo test运行此测试,就像我们之前编写的测试一样。第一次运行可能需要更长时间,因为Command::cargo_bin("f789")需要编译我们的main二进制文件。

    生成测试文件

    我们刚刚看到的测试仅检查当输入文件不存在时,我们的程序是否会写出错误消息。现在让我们测试一下我们是否确实会打印出我们在文件中找到的匹配项!

    我们需要有一个文件,我们知道其内容,以便我们知道我们的程序应该返回什么,并在我们的代码中检查这个期望。

    • 一个想法是向项目中添加一个具有自定义内容的文件,并在我们的测试中使用它。
    • 另一个方法是在我们的测试中创建临时文件。

    为了创建这些临时文件,我们将使用assert_fs crate。让我们将其添加到Cargo.toml中的dev-dependencies中:

    assert_fs = "1.0.13"
    

    这是一个新的测试案例(我们可以在其他测试案例下面编写),它首先创建一个临时文件(一个具名(named)文件,所以我们可以得到它的路径),然后用一些文本填充它,然后运行我们的程序,看看我们是否得到了正确的输出。当文件超出作用域时(在函数的末尾),实际的临时文件将自动被删除。

    use assert_cmd::prelude::*;
    use assert_fs::prelude::*;
    use predicates::prelude::*;
    use std::process::Command;
    
    #[test]
    fn find_content_in_file() -> Result {
        let file = assert_fs::NamedTempFile::new("sample.txt")?;
        file.write_str("111n222n333n4444 11")?;
    
        let mut cmd = Command::cargo_bin("f789")?;
        cmd.arg("11").arg(file.path());
        cmd.assert()
            .success()
            .stdout(predicate::str::contains("111n4444 11"));
    
        Ok(())
    }
    
    

    运行cargo test,代码运行结果如下。

    3. 打包并发布 Rust 项目

    经过,我们通过单元测试和黑盒测试后,我们确认,我们的项目已经功能完备了。是骡子是马拉出来遛遛现在是打包和发布的时候了!

    下面我们看看发布Rust的几种方式。

    最快:cargo publish

    使用cargo发布应用程序是最简单的方法。我们还记得我们如何将外部依赖项添加到项目中吗?cargo会从其默认的包管理器crates.io下载它们。借助cargo publish,我们也可以将crate发布到crates.io。这适用于所有crate,包括具有二进制目标的crate

    crate发布到crates.io相当简单:

  • 如果尚未创建crates.io帐户,需要创建一个帐户。目前,可以通过在GitHub上授权来完成

  • 本地计算机上,登录cargo登录,为此,我们需要到crates.io帐户页面,创建一个新令牌,然后运行cargo login
    创建token

  • 本地登录

    登录成功

  • 确认Cargo.toml信息,确保我们已添加了必要的元数据
    [package]
    name = "f789"
    version = "0.1.0"
    authors = ["Your Name "]
    license = "MIT OR Apache-2.0"
    description = "文件搜索工具"
    readme = "README.md"
    homepage = "https://github.com/you/f789"
    repository = "https://github.com/you/f789"
    keywords = ["cli", "search"]
    categories = ["command-line-utilities"]
    
  • 使用cargo publish进行发布

    发布成功后,就可以在crates.io中查看

    如果你是首次在crates.io发布,你需要验证一下邮箱
  • 如果想了解可以在cargo的发布指南中了解更多信息。

    如何从crates.io安装二进制文件

    我们已经了解了如何将crate发布到crates.io,我们可能想知道如何安装它。与库不同,cargo会在运行cargo build(或类似的命令)时为我们下载和编译库,我们需要明确告诉它要安装二进制文件。

    使用cargo install 可以实现这一点。默认情况下,它会下载crate,编译其中包含的所有二进制目标(以release模式进行,所以可能需要一些时间),并将它们复制到~/.cargo/bin/目录中。

    还可以从git存储库安装crate,仅安装crate的特定二进制文件,并指定替代目录以进行安装。

    何时使用它

    cargo install是一种安装二进制crate的简单方法。对于Rust开发人员来说非常方便,但有一些重要的缺点:由于它总是从头开始编译我们的源代码,因此使用我们的工具的用户需要在其计算机上安装Rustcargo和项目所需的所有其他系统依赖项。编译大型Rust代码库可能也需要一些时间。

    使用cargo install f789按照

    安装成功,并默认存储到/Users/xxx/.cargo/bin

    我们现在可以随意打开一个命令行,并且按照我们之前代码逻辑,f789 front text.txt就可以查看运行结果了。

    大家可以忽略上面截图中git部分的。我为了省事,直接在源代码的目录中,进行了上述的操作。其实上述操作可以在任何终端中运行。

    分发二进制文件

    Rust是一种编译为本机代码的语言,并默认情况下静态链接所有依赖项。当我们在包含名为f789的二进制文件的项目上运行cargo build时,我们将得到一个名为f789的二进制文件。

    • 使用cargo build,它将位于target/debug/f789
    • 当我们运行cargo build --release时,它将位于target/release/f789

    这意味着,我们可以将这个文件发送给与我们运行相同操作系统的人,他们就可以运行它。

    它解决了cargo install的两个缺点:用户的计算机上不需要安装Rust,并且不是需要一分钟才能编译,他们可以立即运行二进制文件。

    因此,正如我们所看到的,cargo build已经为我们构建了二进制文件。唯一的问题是,默认情况下,这些二进制文件不能保证在所有有趣的平台上运行。如果我们在Windows计算机上运行cargo build,我们不会得到默认情况下在Mac上运行的二进制文件。

    在 CI 上构建二进制版本

    如果我们的工具是开源的并托管在GitHub上,那么设置免费的CI(持续集成)服务(如Travis CI)非常容易。这基本上是在虚拟机中每次我们推送更改到我们的存储库时运行设置命令。这些命令是什么以及它们运行在哪种类型的机器上是可配置的。

    我们还可以使用此功能构建二进制文件并将其上传到GitHub

    • 首先,我们运行cargo build --release并将二进制文件上传到某个位置
    • 其次,我们仍然需要确保我们构建的二进制文件与尽可能多的系统兼容。
      • 例如,在Linux上,我们可以编译而不是为当前系统编译,而是为x86_64-unknown-linux-musl目标编译,以避免依赖默认系统库。
      • macOS上,我们可以将MACOSX_DEPLOYMENT_TARGET设置为10.7,只依赖于版本10.7及更早版本中存在的系统功能。

    另一种方法是使用包含构建二进制文件所需工具的预构建(Docker)映像。这允许我们轻松地针对更多的异构平台进行定位。trust项目包含可以包含在我们的项目中的脚本以及设置的说明。它还包括使用AppVeyorWindows支持。

    如果我们只想在本地设置并在自己的计算机上生成发布文件,请仍然查看trust。它在内部使用cross,它的工作方式类似于cargo,但将命令转发到Docker容器内部的cargo进程。这些映像的定义也可在cross的存储库中找到。

    何时使用它

    一般来说,拥有二进制发布版本是一个好主意,几乎没有任何不利因素。它不能解决用户必须手动安装和更新工具的问题,但他们可以快速获取最新的发布版本,而无需安装Rust

    将应用程序放入包存储库

    迄今为止,我们看到的两种方法都不是我们通常在计算机上安装软件的方式。特别是大多数操作系统上的全局软件包管理器,我们可以使用这些管理器来安装命令行工具。对用户来说:如果他们可以以与安装其他工具相同的方式安装程序,那么就无需考虑如何安装我们的程序。这些软件包管理器还允许用户在新版本可用时更新其程序。

    难点在于,支持不同的系统意味着我们必须查看这些不同的系统如何工作。对于某些系统,只需向存储库添加一个文件(例如,为macOSbrew添加一个Formula文件),但对于其他系统,我们通常需要自己发送补丁并将我们的工具添加到它们的存储库中。有一些有用的工具,如cargo-bundle、cargo-deb和cargo-aur,但描述它们的工作原理以及如何正确为这些不同的系统打包我们的工具超出了本章的范围。

    代码展示

    src/main.rs

    use anyhow::{Context, Result};
    use clap::Parser;
    use std::fs::File;
    use std::io::{self, BufRead};
    use std::path::PathBuf;
    
    /// 在文件中搜索模式并显示包含它的行。
    #[derive(Parser)]
    struct Cli {
        /// 要查找的模式
        pattern: String,
        /// 要读取的文件的路径
        path: PathBuf,
    }
    
    fn main() -> Result {
        let args = Cli::parse();
    
        // 打开文件并创建一个 BufReader 来逐行读取
        let file = File::open(&args.path).with_context(|| format!("无法打开文件 {:?}", &args.path))?;
        let reader = io::BufReader::new(file);
    
        let stdout = io::stdout();
        let stdout_lock = stdout.lock();
        let handle = io::BufWriter::new(stdout_lock);
    
        let content = reader
            .lines()
            .collect::()?
            .join("n");
    
        f789::find_matches(&content, &args.pattern, handle)?;
    
        Ok(())
    }
    

    src/lib.rs

    use anyhow::Result;
    use indicatif::ProgressBar;
    use std::io::Write;
    pub fn find_matches(content: &str, pattern: &str, mut writer: impl Write) -> Result {
        let pb = ProgressBar::new(100);
        for line in content.lines() {
            do_hard_work();
            pb.println(format!("[+] 查找到了 #{:?}项", line));
            pb.inc(1);
            if line.contains(pattern) {
                writeln!(writer, "{}", line)?;
            }
        }
        Ok(())
    }
    
    fn do_hard_work() {
        std::thread::sleep(std::time::Duration::from_millis(250));
    }
    

    后记

    分享是一种态度。

    参考资料:

  • 用Rust 写一个命令行
  • crates
  • 全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

    相关文章

    服务器端口转发,带你了解服务器端口转发
    服务器开放端口,服务器开放端口的步骤
    产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
    如何使用 WinGet 下载 Microsoft Store 应用
    百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
    百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

    发布评论