标签 构建 下的文章

这些方便的 Go 构建选项可以帮助你更好地理解 Go 的编译过程。

学习一门新的编程语言最令人欣慰的部分之一,就是最终运行了一个可执行文件,并获得预期的输出。当我开始学习 Go 这门编程语言时,我先是阅读一些示例程序来熟悉语法,然后是尝试写一些小的测试程序。随着时间的推移,这种方法帮助我熟悉了编译和构建程序的过程。

Go 的构建选项提供了更好地控制构建过程的方法。它们还可以提供额外的信息,帮助把这个过程分成更小的部分。在这篇文章中,我将演示我所使用的一些选项。注意:我使用的“ 构建 build ”和“ 编译 compile ”这两个词是同一个意思。

开始使用 Go

我使用的 Go 版本是 1.16.7。但是,这里给出的命令应该也能在最新的版本上运行。如果你没有安装 Go,你可以从 Go 官网 上下载它,并按照说明进行安装。你可以通过打开一个命令提示符,并键入下面的命令来验证你所安装的版本:

$ go version

你应该会得到类似下面这样的输出,具体取决于你安装的版本:

go version go1.16.7 linux/amd64

基本的 Go 程序的编译和执行方法

我将从一个在屏幕上简单打印 “Hello World” 的 Go 程序示例开始,就像下面这样:

$ cat hello.go
package main

import "fmt"

func main() {
    fmt.Println("Hello World")
}

在讨论更高级的选项之前,我将解释如何编译这个 Go 示例程序。我使用了 build 命令,后面跟着 Go 程序的源文件名,本例中是 hello.go,就像下面这样:

$ go build hello.go

如果一切工作正常,你应该看到在你的当前目录下创建了一个名为 hello 的可执行文件。你可以通过使用 file 命令验证它是 ELF 二进制可执行格式(在 Linux 平台上)。你也可以直接执行它,你会看到它输出 “Hello World”。

$ ls
hello  hello.go

$ file ./hello
./hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

$ ./hello
Hello World

Go 提供了一个方便的 run 命令,以便你只是想看看程序是否能正常工作,并获得预期的输出,而不想生成一个最终的二进制文件。请记住,即使你在当前目录中没有看到可执行文件,Go 仍然会在某个地方编译并生成可执行文件并运行它,然后把它从系统中删除。我将在本文后面的章节中解释。

$ go run hello.go
Hello World

$ ls
hello.go

更多细节

上面的命令就像一阵风一样,一下子就运行完了我的程序。然而,如果你想知道 Go 在编译这些程序的过程中做了什么,Go 提供了一个 -x 选项,它可以打印出 Go 为产生这个可执行文件所做的一切。

简单看一下你就会发现,Go 在 /tmp 内创建了一个临时工作目录,并生成了可执行文件,然后把它移到了 Go 源程序所在的当前目录。

$ go build -x hello.go

WORK=/tmp/go-build1944767317
mkdir -p $WORK/b001/

<< snip >>

mkdir -p $WORK/b001/exe/
cd .
/usr/lib/golang/pkg/tool/linux_amd64/link -o $WORK \
/b001/exe/a.out -importcfg $WORK/b001 \
/importcfg.link -buildmode=exe -buildid=K26hEYzgDkqJjx2Hf-wz/\
nDueg0kBjIygx25rYwbK/W-eJaGIOdPEWgwC6o546 \
/K26hEYzgDkqJjx2Hf-wz -extld=gcc /root/.cache/go-build /cc \
/cc72cb2f4fbb61229885fc434995964a7a4d6e10692a23cc0ada6707c5d3435b-d
/usr/lib/golang/pkg/tool/linux_amd64/buildid -w $WORK \
/b001/exe/a.out # internal
mv $WORK/b001/exe/a.out hello
rm -r $WORK/b001/

这有助于解决在程序运行后却在当前目录下没有生成可执行文件的谜团。使用 -x 显示可执行文件确实在 /tmp 工作目录下创建并被执行了。然而,与 build 命令不同的是,可执行文件并没有移动到当前目录,这使得看起来没有可执行文件被创建。

$ go run -x hello.go


mkdir -p $WORK/b001/exe/
cd .
/usr/lib/golang/pkg/tool/linux_amd64/link -o $WORK/b001 \
/exe/hello -importcfg $WORK/b001/importcfg.link -s -w -buildmode=exe -buildid=hK3wnAP20DapUDeuvAAS/E_TzkbzwXz6tM5dEC8Mx \
/7HYBzuaDGVdaZwSMEWAa/hK3wnAP20DapUDeuvAAS -extld=gcc \
/root/.cache/go-build/75/ \
7531fcf5e48444eed677bfc5cda1276a52b73c62ebac3aa99da3c4094fa57dc3-d
$WORK/b001/exe/hello
Hello World

模仿编译而不产生可执行文件

假设你不想编译程序并产生一个实际的二进制文件,但你确实想看到这个过程中的所有步骤。你可以通过使用 -n 这个构建选项来做到这一点,该选项会打印出通常的执行步骤,而不会实际创建二进制文件。

$ go build -n hello.go

保存临时目录

很多工作都发生在 /tmp 工作目录中,一旦可执行文件被创建和运行,它就会被删除。但是如果你想看看哪些文件是在编译过程中创建的呢?Go 提供了一个 -work 选项,它可以在编译程序时使用。-work 选项除了运行程序外,还打印了工作目录的路径,但它并不会在这之后删除工作目录,所以你可以切换到该目录,检查在编译过程中创建的所有文件。

$ go run -work hello.go
WORK=/tmp/go-build3209320645
Hello World

$ find /tmp/go-build3209320645
/tmp/go-build3209320645
/tmp/go-build3209320645/b001
/tmp/go-build3209320645/b001/importcfg.link
/tmp/go-build3209320645/b001/exe
/tmp/go-build3209320645/b001/exe/hello

$ /tmp/go-build3209320645/b001/exe/hello
Hello World

其他编译选项

如果说,你想手动编译程序,而不是使用 Go 的 buildrun 这两个方便的命令,最后得到一个可以直接由你的操作系统(这里指 Linux)运行的可执行文件。那么,你该怎么做呢?这个过程可以分为两部分:编译和链接。你可以使用 tool 选项来看看它是如何工作的。

首先,使用 tool compile 命令产生结果的 ar 归档文件,它包含了 .o 中间文件。接下来,对这个 hello.o 文件执行 tool link 命令,产生最终的可执行文件,然后你就可以运行它了。

$ go tool compile hello.go

$ file hello.o
hello.o: current ar archive

$ ar t hello.o
__.PKGDEF
_go_.o

$ go tool link -o hello hello.o

$ file hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

$ ./hello
Hello World

如果你想进一步查看基于 hello.o 文件产生可执行文件的链接过程,你可以使用 -v 选项,它会搜索每个 Go 可执行文件中包含的 runtime.a 文件。

$ go tool link -v -o hello hello.o
HEADER = -H5 -T0x401000 -R0x1000
searching for runtime.a in /usr/lib/golang/pkg/linux_amd64/runtime.a
82052 symbols, 18774 reachable
        1 package symbols, 1106 hashed symbols, 77185 non-package symbols, 3760 external symbols
81968 liveness data

交叉编译选项

现在我已经解释了 Go 程序的编译过程,接下来,我将演示 Go 如何通过在实际的 build 命令之前提供 GOOSGOARCH 这两个环境变量,来允许你构建针对不同硬件架构和操作系统的可执行文件。

这有什么用呢?举个例子,你会发现为 ARM(arch64)架构制作的可执行文件不能在英特尔(x86\_64)架构上运行,而且会产生一个 Exec 格式错误。

下面的这些选项使得生成跨平台的二进制文件变得小菜一碟:

$ GOOS=linux GOARCH=arm64 go build hello.go

$ file ./hello
./hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, not stripped

$ ./hello
bash: ./hello: cannot execute binary file: Exec format error

$ uname -m
x86_64

你可以阅读我之前的博文,以更多了解我在 使用 Go 进行交叉编译 方面的经验。

查看底层汇编指令

源代码并不会直接转换为可执行文件,尽管它生成了一种中间汇编格式,然后最终被组装为可执行文件。在 Go 中,这被映射为一种中间汇编格式,而不是底层硬件汇编指令。

要查看这个中间汇编格式,请在使用 build 命令时,提供 -gcflags 选项,后面跟着 -S。这个命令将会显示使用到的汇编指令:

$ go build -gcflags="-S" hello.go
# command-line-arguments
"".main STEXT size=138 args=0x0 locals=0x58 funcid=0x0
        0x0000 00000 (/test/hello.go:5) TEXT    "".main(SB), ABIInternal, $88-0
        0x0000 00000 (/test/hello.go:5) MOVQ    (TLS), CX
        0x0009 00009 (/test/hello.go:5) CMPQ    SP, 16(CX)
        0x000d 00013 (/test/hello.go:5) PCDATA  $0, $-2
        0x000d 00013 (/test/hello.go:5) JLS     128

<< snip >>

你也可以使用 objdump -s 选项,来查看已经编译好的可执行程序的汇编指令,就像下面这样:

$ ls
hello  hello.go

$ go tool objdump -s main.main hello
TEXT main.main(SB) /test/hello.go
  hello.go:5            0x4975a0                64488b0c25f8ffffff      MOVQ FS:0xfffffff8, CX                  
  hello.go:5            0x4975a9                483b6110                CMPQ 0x10(CX), SP                       
  hello.go:5            0x4975ad                7671                    JBE 0x497620                            
  hello.go:5            0x4975af                4883ec58                SUBQ $0x58, SP                          
  hello.go:6            0x4975d8                4889442448              MOVQ AX, 0x48(SP)                       

<< snip >>

分离二进制文件以减少其大小

Go 的二进制文件通常比较大。例如, 一个简单的 “Hello World” 程序将会产生一个 1.9M 大小的二进制文件。

$ go build hello.go
$
$ du -sh hello
1.9M    hello
$
$ file hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
$

为了减少生成的二进制文件的大小,你可以分离执行过程中不需要的信息。使用 -ldflags-s -w 选项可以使生成的二进制文件略微变小为 1.3M。

$ go build -ldflags="-s -w" hello.go
$
$ du -sh hello
1.3M    hello
$
$ file hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped
$

总结

我希望这篇文章向你介绍了一些方便的 Go 编译选项,同时帮助了你更好地理解 Go 编译过程。关于构建过程的其他信息和其他有趣的选项,请参考 Go 命令帮助:

$ go help build

题图由 Ashraf ChembanPixabay 上发布。


via: https://opensource.com/article/22/4/go-build-options

作者:Gaurav Kamathe 选题:lkxed 译者:lkxed 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

提供一个适当的 CMake 配置文件来使其他人可以更容易地构建、使用和贡献你的项目。

 title=

这篇文章是使用开源 DevOps 工具进行 C/C++ 开发系列文章的一部分。如果你从一开始就把你的项目建立在一个功能强大的工具链上,你的开发会更快和更安全。除此之外,这会使别人更容易地参与你的项目。在这篇文章中,我将搭建一个基于 CMakeVSCodium 的 C/C++ 构建系统。像往常一样,相关的示例代码可以在 GitHub 上找到。

我已经测试了在本文中描述的步骤。这是一种适用于所有平台的解决方案。

为什么用 CMake ?

CMake 是一个构建系统生成器,可以为你的项目创建 Makefile。乍一看简单的东西可能相当地复杂。在较高的层次上,你可以定义你的项目的各个部分(可执行文件、库)、编译选项(C/C++ 标准、优化、架构)、依赖关系项(头文件、库),和文件级的项目结构。CMake 使用的这些信息可以在文件 CMakeLists.txt 中获取,它使用一种特殊的描述性语言编写。当 CMake 处理这个文件时,它将自动地侦测在你的系统上已安装的编译器,并创建一个用于启动它的 Makefile 文件。

此外,在 CMakeLists.txt 中描述的配置,能够被很多编辑器读取,像 QtCreator、VSCodium/VSCode 或 Visual Studio 。

示例程序

我们的示例程序是一个简单的命令行工具:它接受一个整数来作为参数,输出一个从 1 到所提供输入值的范围内的随机排列的数字。

$ ./Producer 10
3 8 2 7 9 1 5 10 6 4 

在我们的可执行文件中的 main() 函数,我们只处理输入的参数,如果没有提供一个值(或者一个不能被处理的值)的话,就退出程序。

int main(int argc, char** argv){

    if (argc != 2) {
        std::cerr << "Enter the number of elements as argument" << std::endl;
        return -1;
    }

    int range = 0;
    
    try{
        range = std::stoi(argv[1]);
    }catch (const std::invalid_argument&){
        std::cerr << "Error: Cannot parse \"" << argv[1] << "\" ";
        return -1;
    }

    catch (const std::out_of_range&) {
        std::cerr << "Error: " << argv[1] << " is out of range";
        return -1;
    }

    if (range <= 0) {
        std::cerr << "Error: Zero or negative number provided: " << argv[1];
        return -1;
    }

    std::stringstream data;
    std::cout << Generator::generate(data, range).rdbuf();
}

producer.cpp

实际的工作是在 生成器 中完成的,它将被编译,并将作为一个静态库来链接到我们的Producer 可执行文件。

std::stringstream &Generator::generate(std::stringstream &stream, const int range) {
    std::vector<int> data(range);
    std::iota(data.begin(), data.end(), 1);

    std::random_device rd;
    std::mt19937 g(rd());

    std::shuffle(data.begin(), data.end(), g);

    for (const auto n : data) {

        stream << std::to_string(n) << " ";
    }

    return stream;
}

Generator.cpp

函数 generate 引用一个 std::stringstream 和一个整数来作为一个参数。根据整数 range 的值 n,制作一个在 1n 的范围之中的整数向量,并随后打乱。接下来打乱的向量值转换成一个字符串,并推送到 stringstream 之中。该函数返回与作为参数传递相同的 stringstream 引用。

顶层的 CMakeLists.txt

顶层的 CMakeLists.txt 的是我们项目的入口点。在子目录中可能有多个 CMakeLists.txt 文件(例如,与项目所相关联的库或其它可执行文件)。我们先一步一步地浏览顶层的 CMakeLists.txt

第一行告诉我们处理文件所需要的 CMake 的版本、项目名称及其版本,以及预定的 C++ 标准。

cmake_minimum_required(VERSION 3.14)

project(CPP_Testing_Sample VERSION 1.0)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

我们用下面一行告诉 CMake 去查看子目录 Generator。这个子目录包括构建 Generator 库的所有信息,并包含它自身的一个 CMakeLists.txt 。我们很快就会谈到这个问题。

add_subdirectory(Generator)

现在,我们将涉及一个绝对特别的功能: CMake 模块 。加载模块可以扩展 CMake 功能。在我们的项目中,我们加载了 FetchContent 模块,这能使我们能够在 CMake 运行时下载外部的资源,在我们的示例中是 GoogleTest

include(FetchContent)

FetchContent_Declare(
  googletest
  URL https://github.com/google/googletest/archive/bb9216085fbbf193408653ced9e73c61e7766e80.zip
)
FetchContent_MakeAvailable(googletest)

在接下来的部分中,我们会做一些我们通常在普通的 Makefile 中会做的事: 指定要构建的二进制文件、它们相关的源文件、应该链接的库,以及编译器可以找到头文件的目录。

add_executable(Producer Producer.cpp)

target_link_libraries(Producer PUBLIC Generator)

target_include_directories(Producer PUBLIC "${PROJECT_BINARY_DIR}")

通过下面的语句,我们使 CMake 来在构建文件夹中创建一个名称为 compile_commands.json 的文件。这个文件会展示项目的每个文件的编译器选项。在 VSCodium 中加载该文件,会告知 IntelliSense 功能在哪里查找头文件(查看 文档)。

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

最后的部分为我们的项目定义一些测试。项目使用先前加载的 GoogleTest 框架。单元测试的整个话题将会划归到另外一篇文章。

enable_testing()

add_executable(unit_test unit_test.cpp)

target_link_libraries(unit_test gtest_main)

include(GoogleTest)

gtest_discover_tests(unit_test)

库层次的 CMakeLists.txt

现在,我们来看看包含同名库的子目录 Generator 中的 CMakeLists.txt 文件。这个 CMakeLists.txt 文件的内容更简短一些,除了单元测试相关的命令外,它仅包含 2 条语句。

add_library(Generator STATIC Generator.cpp Generator.h)
target_include_directories(Generator INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})

我们使用 add_library(...) 来定义一个新的构建目标:静态的 Generator 库。我们使用语句 target_include_directories(...) 来把当前子目录添加到其它构建目标的头文件的搜索路径之中。我们也具体指定这个属性的范围为类型 INTERFACE:这意味着该属性仅影响链接到这个库的构建目标,而不是库本身。

开始使用 VSCodium

通过使用 CMakeLists.txt 文件中的信息,像 VSCodium 一样的 IDE 可以相应地配置构建系统。如果你还没有使用 VSCodium 或 VS Code 的经验,这个示例项目会是一个很好的起点。首先,转到它们的 网站 ,然后针对你的系统下载最新的安装软件包。打开 VSCodium 并导航到 “ 扩展 Extensions ” 标签页。

为了正确地构建、调试和测试项目,搜索下面的扩展并安装它们:

 title=

如果尚未完成,通过单击起始页的 “ 克隆 Git 存储库 Clone Git Repository ” 来克隆存储库。

 title=

或者手动输入:

git clone https://github.com/hANSIc99/cpp_testing_sample.git

之后,通过输入如下内容来签出标签 devops_1

git checkout tags/devops_1

或者,通过单击 “main” 分支按钮(红色框),并从下拉菜单(黄色框)中选择标签。

 title=

在你打开 VSCodium 内部中的存储库的根文件夹后,CMake Tools 扩展会侦测 CMakeLists.txt 文件并立即扫描你的系统寻找合适的编译器。你现在可以单击屏幕的底部的 “ 构建 Build ” 按钮(红色框)来开始构建过程。你也可以通过单击底部区域的按钮(黄色框)标记来更改编译器,它显示当前活动的编译器。

 title=

要开始调试 Producer 可执行文件,单击调试器符号(黄色框)并从下拉菜单中选择 “ 调试 Debug Producer”(绿色框)。

 title=

如上所述,Producer 可执行文件要求将元素的数量作为一个命令行的参数。命令行参数可以在 .vscode/launch.json 中具体指定。

 title=

好了,你现在能够构建和调试项目了。

结束语

归功于 CMake ,不管你正在运行哪种操作系统,上述步骤应该都能工作。特别是使用与 CMake 相关的扩展,VSCodium 变成了一个强大的 IDE 。我没有提及 VSCodium 的 Git 集成,是因为你已经能够在网络上查找很多的资源。我希望你可以看到:提供一个适当的 CMake 配置文件可以使其他人更容易地构建、使用和贡献于你的项目。在未来的文章中,我将介绍单元测试和 CMake 的测试实用程序 ctest


via: https://opensource.com/article/22/1/devops-cmake

作者:Stephan Avenwedde 选题:lujun9972 译者:robsean 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

这是一篇快速提示,旨在给 Ubuntu 的新用户解释构建基础包是什么、它的用处和安装步骤。

在 Ubuntu 中安装构建基础包(build-essential),只需要在终端中简单输入这个命令:

sudo apt update && sudo apt install build-essential

但围绕它有几个问题,你可能想知道答案:

  • 什么是构建基础包?
  • 它包含什么内容?
  • 为什么要安装它(如果安装的话)?
  • 如何安装它?
  • 如何删除它?

什么是 Ubuntu 中的构建基础包?

构建基础包(build-essential)实际上是属于 Debian 的。在它里面其实并不是一个软件。它包含了创建一个 Debian 包(.deb)所需的软件包列表。这些软件包包括 libcgccg++makedpkg-dev 等。构建基础包包含这些所需的软件包作为依赖,所以当你安装它时,你只需一个命令就能安装所有这些软件包。

请不要认为构建基础包是一个可以在一个命令中神奇地安装从 Ruby 到 Go 的所有开发工具的超级软件包。它包含一些开发工具,但不是全部。

你为什么要安装构建基础包?

它用来从应用的源代码创建 DEB 包。一个普通用户不会每天都去创建 DEB 包,对吗?

然而,有些用户可能会使用他们的 Ubuntu Linux 系统进行软件开发。如果你想 在 Ubuntu 中运行 c 程序,你需要 gcc 编译器。如果你想 在 Ubuntu 中运行 C++ 程序,你需要 g++ 编译器。如果你要使用一个不寻常的、只能从源代码中获得的软件,你的系统会抛出 “make 命令未找到的错误”,因为你需要先安装 make 工具。

当然,所有这些都可以单独安装。然而,利用构建基础包的优势,一次性安装所有这些开发工具要容易得多。这就是你得到的好处。

这就像 ubuntu-restricted-extras 包允许你一次安装几个媒体编解码器

现在你知道了这个包的好处,让我们看看如何安装它。

在 Ubuntu Linux 中安装构建基础包

在 Ubuntu 中按 Ctrl+Alt+T 快捷键打开终端,输入以下命令:

sudo apt update

使用 sudo 命令,你会被要求输入你的账户密码。当你输入时,屏幕上没有任何显示。这没问题。这在大多数 Linux 系统中都是这样的。盲打输入你的密码,然后按回车键。

apt update 命令刷新了本地软件包的缓存。这对于一个新安装的 Ubuntu 来说是必不可少的。

之后,运行下面的命令来安装构建基础包:

sudo apt install build-essential

它应该显示所有要安装的软件包。当要求确认时按 Y

等待安装完成。就好了。

从 Ubuntu 中删除构建基础包

保留这些开发工具不会损害你的系统。但如果你的磁盘空间不足,你可以考虑删除它。

在 Ubuntu 中,由于有 apt remove 命令,删除软件很容易:

sudo apt remove build-essential

运行 autoremove 命令来删除剩余的依赖包也是一个好主意:

sudo apt autoremove

你现在知道了所有关于构建基础包的基础(双关语)。请享受它吧~


via: https://itsfoss.com/build-essential-ubuntu/

作者:Abhishek Prakash 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

大家好!每隔一段时间,我就会发现一款我非常喜欢的新软件,今天我想说说我最近喜欢的一款软件:ninja

增量构建很有用

我做了很多小项目,在这些项目中,我想设置增量构建。例如,现在我正在写一本关于 bash 的杂志,杂志的每一页都有一个 .svg文件。我需要将 SVG 转换为 PDF,我的做法是这样的:

for i in *.svg
do
    svg2pdf $i $i.pdf # or ${i/.svg/.pdf} if you want to get really fancy
done

这很好用,但是我的 svg2pdf 脚本有点慢(它使用 Inkscape),而且当我刚刚只更新了一页的时候,必须等待 90 秒或者其他什么时间来重建所有的 PDF 文件,这很烦人。

构建系统是让人困惑的

在过去,我对使用 makebazel 这样的构建系统来做我的小项目一直很反感,因为 bazel 是个大而复杂的东西,而 make 对我来说感觉有点神秘。我真的不想使用它们中的任何一个。

所以很长时间以来,我只是写了一个 bash 脚本或者其他的东西来进行构建,然后就认命了,有时候只能等一分钟。

ninja 是一个极其简单的构建系统

ninja 并不复杂!以下是我所知道的关于 ninja 构建文件的语法:创建一个 rule 和一个 build

rule 有一个命令(command)和描述(description)参数(描述只是给人看的,所以你可以知道它在构建你的代码时在做什么)。

rule svg2pdf
  command = inkscape $in --export-text-to-path --export-pdf=$out
  description = svg2pdf $in $out

build 的语法是 build output_file: rule_name input_files。下面是一个使用 svg2pdf 规则的例子。输出在规则中的 $out 里,输入在 $in 里。

build pdfs/variables.pdf: svg2pdf variables.svg

这就完成了!如果你把这两个东西放在一个叫 build.ninja 的文件里,然后运行 ninja,ninja 会运行 inkscape variables.svg --export-text-to-path --export-pdf=pdfs/variables.pdf。然后如果你再次运行它,它不会运行任何东西(因为它可以告诉你已经构建了 pdfs/variables.pdf,而且是最新的)。

Ninja 还有一些更多的功能(见手册),但我还没有用过。它最初是为 Chromium 构建的,所以即使只有一个小的功能集,它也能支持大型构建。

ninja 文件通常是自动生成的

ninja 的神奇之处在于,你不必使用一些混乱的构建语言,它们很难记住,因为你不经常使用它(比如 make),相反,ninja 语言超级简单,如果你想做一些复杂的事情,那么你只需使用任意编程语言生成你想要的构建文件。

我喜欢写一个 build.py 文件,或者像这样的文件,创建 ninja 的构建文件,然后运行 ninja

with open('build.ninja', 'w') as ninja_file:
    # write some rules
    ninja_file.write("""
rule svg2pdf
  command = inkscape $in --export-text-to-path --export-pdf=$out
  description = svg2pdf $in $out
""")

    # some for loop with every file I need to build
    for filename in things_to_convert:
        ninja_file.write(f"""
build {filename.replace('svg', 'pdf')}: svg2pdf {filename}
""")

# run ninja
import subprocess
subprocess.check_call(['ninja'])

我相信有一堆 ninja 的最佳实践,但我不知道。对于我的小项目而言,我发现它很好用。

meson 是一个生成 ninja 文件的构建系统

我对 Meson 还不太了解,但最近我在构建一个 C 程序 (plocate,一个比 locate 更快的替代方案)时,我注意到它有不同的构建说明,而不是通常的 ./configure; make; make install

meson builddir
cd builddir
ninja

看起来 Meson 是一个可以用 ninja 作为后端的 C/C++/Java/Rust/Fortran 构建系统。

就是这些!

我使用 ninja 已经有几个月了。我真的很喜欢它,而且它几乎没有给我带来让人头疼的构建问题,这让我感觉非常神奇。


via: https://jvns.ca/blog/2020/10/26/ninja--a-simple-way-to-do-builds/

作者:Julia Evans 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

搭建一个通过容器分发应用的可复用系统可能很复杂,但这儿有个好方法。

一个用于将源代码转换成可运行的应用的构建系统是由工具和流程共同组成。在转换过程中还涉及到代码的受众从软件开发者转变为最终用户,无论最终用户是运维的同事还是部署的同事。

在使用容器搭建了一些构建系统后,我觉得有一个不错的可复用的方法值得分享。虽然这些构建系统被用于编译机器学习算法和为嵌入式硬件生成可加载的软件镜像,但这个方法足够抽象,可用于任何基于容器的构建系统。

这个方法是以一种易于使用和维护的方式搭建或组织构建系统,但并不涉及处理特定编译器或工具容器化的技巧。它适用于软件开发人员构建软件,并将可维护镜像交给其他技术人员(无论是系统管理员、运维工程师或者其他一些头衔)的常见情况。该构建系统被从终端用户中抽象出来,这样他们就可以专注于软件。

为什么要容器化构建系统?

搭建基于容器的可复用构建系统可以为软件团队带来诸多好处:

  • 专注:我希望专注于应用的开发。当我调用一个工具进行“构建”时,我希望这个工具集能生成一个随时可用的二进制文件。我不想浪费时间在构建系统的查错上。实际上,我宁愿不了解,或者说不关心构建系统。
  • 一致的构建行为:无论在哪种使用情况下,我都想确保整个团队使用相同版本的工具集并在构建时得到相同的结果。否则,我就得不断地处理“我这咋就是好的”的麻烦。在团队项目中,使用相同版本的工具集并对给定的输入源文件集产生一致的输出是非常重要。
  • 易于部署和升级:即使向每个人都提供一套详细说明来安装一个项目的工具集,也可能会有人翻车。问题也可能是由于每个人对自己的 Linux 环境的个性化修改导致的。在团队中使用不同的 Linux 发行版(或者其他操作系统),情况可能还会变得更复杂。当需要将工具集升级到下一版本时,问题很快就会变得更糟糕。使用容器和本指南将使得新版本升级非常简单。

对我在项目中使用的构建系统进行容器化的这些经验显然很有价值,因为它可以缓解上述问题。我倾向于使用 Docker 作为容器工具,虽然在相对特殊的环境中安装和网络配置仍可能出现问题,尤其是当你在一个使用复杂代理的企业环境中工作时。但至少现在我需要解决的构建系统问题已经很少了。

漫步容器化的构建系统

我创建了一个教程存储库,随后你可以克隆并检查它,或者按照本文内容进行操作。我将逐个介绍存储库中的文件。这个构建系统非常简单(它运行 gcc),从而可以让你专注于这个构建系统结构上。

构建系统需求

我认为构建系统中有两个关键点:

  • 标准化构建调用:我希望能够指定一些形如 /path/to/workdir 的工作目录来构建代码。我希望以如下形式调用构建:
./build.sh /path/to/workdir

为了使得示例的结构足够简单(以便说明),我将假定输出也在 /path/to/workdir 路径下的某处生成。(否则,将增加容器中显示的卷的数量,虽然这并不困难,但解释起来比较麻烦。)

  • 通过 shell 自定义构建调用:有时,工具集会以出乎意料的方式被调用。除了标准的工具集调用 build.sh 之外,如果需要还可以为 build.sh 添加一些选项。但我一直希望能够有一个可以直接调用工具集命令的 shell。在这个简单的示例中,有时我想尝试不同的 gcc 优化选项并查看效果。为此,我希望调用:
./shell.sh /path/to/workdir

这将让我得到一个容器内部的 Bash shell,并且可以调用工具集和访问我的工作目录(workdir),从而我可以根据需要尝试使用这个工具集。

构建系统的架构

为了满足上述基本需求,这是我的构架系统架构:

 title=

在底部的 workdir 代表软件开发者用于构建的任意软件源码。通常,这个 workdir 是一个源代码的存储库。在构建之前,最终用户可以通过任何方式来操纵这个存储库。例如,如果他们使用 git 作为版本控制工具的话,可以使用 git checkout 切换到他们正在工作的功能分支上并添加或修改文件。这样可以使得构建系统独立于 workdir 之外。

顶部的三个模块共同代表了容器化的构建系统。最左边的黄色模块代表最终用户与构建系统交互的脚本(build.shshell.sh)。

在中间的红色模块是 Dockerfile 和相关的脚本 build_docker_image.sh。开发运营者(在这个例子中指我)通常将执行这个脚本并生成容器镜像(事实上我多次执行它直到一切正常为止,但这是另一回事)。然后我将镜像分发给最终用户,例如通过 容器信任注册库 container trusted registry 进行分发。最终用户将需要这个镜像。另外,他们将克隆构建系统的存储库(即一个与教程存储库等效的存储库)。

当最终用户调用 build.sh 或者 shell.sh 时,容器内将执行右边的 run_build.sh 脚本。接下来我将详细解释这些脚本。这里的关键是最终用户不需要为了使用而去了解任何关于红色或者蓝色模块或者容器工作原理的知识。

构建系统细节

把教程存储库的文件结构映射到这个系统结构上。我曾将这个原型结构用于相对复杂构建系统,因此它的简单并不会造成任何限制。下面我列出存储库中相关文件的树结构。文件夹 dockerize-tutorial 能用构建系统的其他任何名称代替。在这个文件夹下,我用 workdir 的路径作参数调用 build.shshell.sh

dockerize-tutorial/
├── build.sh
├── shell.sh
└── swbuilder
    ├── build_docker_image.sh
    ├── install_swbuilder.dockerfile
    └── scripts
        └── run_build.sh

请注意,我上面特意没列出 example_workdir,但你能在教程存储库中找到它。实际的源码通常存放在单独的存储库中,而不是构建工具库中的一部分;本教程为了不必处理两个存储库,所以我将它包含在这个存储库中。

如果你只对概念感兴趣,本教程并非必须的,因为我将解释所有文件。但是如果你继续本教程(并且已经安装 Docker),首先使用以下命令来构建容器镜像 swbuilder:v1

cd dockerize-tutorial/swbuilder/
./build_docker_image.sh
docker image ls  # resulting image will be swbuilder:v1

然后调用 build.sh

cd dockerize-tutorial
./build.sh ~/repos/dockerize-tutorial/example_workdir

下面是 build.sh 的代码。这个脚本从容器镜像 swbuilder:v1 实例化一个容器。而这个容器实例映射了两个卷:一个将文件夹 example_workdir 挂载到容器内部路径 /workdir 上,第二个则将容器外的文件夹 dockerize-tutorial/swbuilder/scripts 挂载到容器内部路径 /scripts 上。

docker container run                              \
    --volume $(pwd)/swbuilder/scripts:/scripts    \
    --volume $1:/workdir                          \
    --user $(id -u ${USER}):$(id -g ${USER})      \
    --rm -it --name build_swbuilder swbuilder:v1  \
    build

另外,build.sh 还会用你的用户名(以及组,本教程假设两者一致)去运行容器,以便在访问构建输出时不出现文件权限问题。

请注意,shell.shbuild.sh 大体上是一致的,除了两点不同:build.sh 会创建一个名为 build_swbuilder 的容器,而 shell.sh 则会创建一个名为 shell_swbuilder 的容器。这样一来,当其中一个脚本运行时另一个脚本被调用也不会产生冲突。

两个脚本之间的另一处关键不同则在于最后一个参数:build.sh 传入参数 buildshell.sh 则传入 shell。如果你看了用于构建容器镜像的 Dockerfile,就会发现最后一行包含了下面的 ENTRYPOINT 语句。这意味着上面的 docker container run 调用将使用 buildshell 作为唯一的输入参数来执行 run_build.sh 脚本。

# run bash script and process the input command
ENTRYPOINT [ "/bin/bash", "/scripts/run_build.sh"]

run\_build.sh 使用这个输入参数来选择启动 Bash shell 还是调用 gcc 来构建 helloworld.c 项目。一个真正的构建系统通常会使用 Makefile 而非直接运行 gcc

cd /workdir

if [ $1 = "shell" ]; then    
    echo "Starting Bash Shell"
    /bin/bash
elif [ $1 = "build" ]; then
    echo "Performing SW Build"
    gcc helloworld.c -o helloworld -Wall
fi

在使用时,如果你需要传入多个参数,当然也是可以的。我处理过的构建系统,构建通常是对给定的项目调用 make。如果一个构建系统有非常复杂的构建调用,则你可以让 run_build.sh 调用 workdir 下最终用户编写的特定脚本。

关于 scripts 文件夹的说明

你可能想知道为什么 scripts 文件夹位于目录树深处而不是位于存储库的顶层。两种方法都是可行的,但我不想鼓励最终用户到处乱翻并修改里面的脚本。将它放到更深的地方是一个让他们更难乱翻的方法。另外,我也可以添加一个 .dockerignore 文件去忽略 scripts 文件夹,因为它不是容器必需的部分。但因为它很小,所以我没有这样做。

简单而灵活

尽管这一方法很简单,但我在几个相当不同的构建系统中使用过,发现它相当灵活。相对稳定的部分(例如,一年仅修改数次的给定工具集)被固定在容器镜像内。较为灵活的部分则以脚本的形式放在镜像外。这使我能够通过修改脚本并将更改推送到构建系统存储库中,轻松修改调用工具集的方式。用户所需要做的是将更改拉到本地的构建系统存储库中,这通常是非常快的(与更新 Docker 镜像不同)。这种结构使其能够拥有尽可能多的卷和脚本,同时使最终用户摆脱复杂性。


via: https://opensource.com/article/20/4/how-containerize-build-system

作者:Ravi Chandran 选题:lujun9972 译者:LazyWolfLin 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出