最近在研究Gradle和Java相关构建的实现,对不同编程语言的应用构建有点兴趣。用不同的编程语言编写的应用程序在运行时有不同的运行机制,有的以二进制方式运行,有的运行在该编程语言的虚拟机上。而构建所做的就是将我们为人类编写的代码转换为机器/程序可以理解的代码。所以,构造的本质就是翻译(~~repeater~~)。PS:本文旨在尝试梳理一下我所知道的搭建知识。部分内容限于对某些编程语言的有限理解,不是很准确。如有偏颇,希望大家指正。引言一:说起Java编译,大部分程序员的复制粘贴生涯都是从hello,world!开始的。刚接触Java的程序员类似:javacHelloWorld.java,当我们依赖其他软件包时,我们需要在编译时和运行时添加classpath来添加依赖。因此,对应的运行命令如下:java-classpath.:libs/joda-time-2.10.6.jarHelloWorld这样我们就可以得到预期的结果:Hello,WorldMillisecondtime:in.getMillis():1599284014762andIf我们需要做一个jar包,需要一个比较复杂的过程:jarcvfmhello.jarmanifest.txtHelloWorld.classlibs/*这个过程涉及几个关键要素:工具链。也就是java和javac,以及相应的Runtime等构建过程。即我需要先执行javac编译,然后通过java命令启动应用。依赖管理。也就是我们的joda-time-2.10.6.jar的location获取和打包时加入的过程。源配置。即转换过程中的class和java过程中的输入输出。引言2:任务和任务输入输出对于一个产品的构建,我们常常将其拆分成一系列的任务,每个任务都有自己的输入输出。当输入改变时,相应的输出也需要改变。接下来,我们只需要安排任务:exports.build=series(clean,parallel(cssTranspile,series(jsTranspile,jsBundle)),parallel(cssMinify,jsMinify),publish);如上图:哪些任务可以并行化,哪些任务需要顺序执行——也可以认为是任务依赖。当然还有watch任务,只在开发时使用,构建时不会用到。以下是Node.js中Gulp构建工具的文件监控示例:,scss);watch('src/*.js',系列(javascript));当两者结合起来,我们就会看到增量任务的概念:只对修改的部分进行编译,以提高构建效率。在这方面做得比较好的就是Gradle,看个官方的示例InputChanges:abstractclassIncrementalReverseTaskextendsDefaultTask{@Incremental@InputDirectoryabstractDirectoryPropertygetInputDir()@OutputDirectoryabstractDirectoryPropertygetOutputDir()@TaskActionvoidexecute(InputChangesinputChanges){inputChanges.getFileChanges(inputDir).each{change->if(change.fileType==FileType.DIRECTORY)返回deftargetFile=outputDir.file(change.normalizedPath).get().asFileif(change.changeType==ChangeType.REMOVED){targetFile.delete()}else{targetFile.text=改变。file.text.reverse()}}}}同样,它也需要我们监听对应的输入输出。稍有不同的是,Gradle会对文件进行索引,每次只提供变化的部分,让我们根据实际需要进行处理。增量构建相关资源:tup是一个基于文件的构建系统,适用于Linux、OSX和Windows。它输入文件的变更列表和有向无环图(DAG),然后处理DAG以执行更新依赖文件所需的适当命令。ninja是一个专注于速度的小型构建系统,类似于GNUMake。SCons是一个用Python语言编写的开源构建系统,类似于GNUMake。Primer3:OptionalDependencyManagement(Hell)关于依赖管理槽,我写过一系列文章,比如:11StrategiesforManagingDependencies,DependencyTwins:ALow-CostDependencySecuritySolution。单从构建的角度来说,依赖的管理是可有可无的。造成这种情况的主要原因是历史上没有任何编程语言考虑过这个问题。因此,在古老的C/C++语言中,构建系统是一个令人头疼的问题。当然,新的Golang也缺乏好的设计。幸运的是,对于依赖管理,这个过程并不复杂:包命名和版本机制包管理服务器构建和运行时依赖管理包冲突处理……构建的抽象就好了,有了上面这一系列的基础知识之后,我们可以看看同一个概念在不同构造系统中的抽象。整合了Bazel、Gradle、Cargo、NPM等之后,就有了一个基本的抽象层:工作空间(workspace)。工作区是一个或多个包的集合,这些包可能共享依赖项、输出目录配置等。典型的例子有Java中的Gradlesettings.gradle,Rust中的Cargo.toml等库。仓库可以映射到Git存储库,代表一个可独立构建的软件。包。项目结构的最小可执行单元。包布局。对应不同的语言和构造系统,用于定义代码的存储位置和结构。产品。也就是说,构建的产品可以是可重复使用的软件包,也可以是可执行的应用程序。任务。定义构建规则并执行它们。为什么常见问题解答中没有项目?在业务领域和技术领域,我们对项目的定义存在一定的歧义。为了减少歧义,我们使用工作空间+仓库来解决这个问题。一个工作区可以被认为是一个完整的商业项目。仓库是一个单一的代码库,它可以是一个库,也可以是一个包含库的完整项目。最好的解决方案是Bazel。工作区工作区是一个或多个包的集合,这些包可能共享依赖关系、输出目录配置等。典型的例子有Java中的Gradlesettings.gradle,Rust中的Cargo.toml等。我们可以把它看成是最终产品,就像Android生成的APK,最后由Rust生成的可执行文件一样。在这个过程中,生成的共享包都是支持这个项目的一部分。先看CMakeLists.txt的目录。我们在工作区的根节点定义了项目,并添加了projectA和projectB。cmake_minimum_required(VERSION3.2.2)project(globalProject)add_subdirectory(projectA)add_subdirectory(projectB)用于生成最终构建产品。类似于Rust中的工作空间:[workspace]members=["adder",]或者前端Yarn中的工作空间:{"private":true,"workspaces":["workspace-a","workspace-b"]}他们都做同样的事情。仓库概念的重新提取源自Bazel。仓库是包的集合。我们可以把它看成是团队的边界,某种意义上也可以看成是一个代码仓库。对于一个庞大的项目,它的代码来源是多种多样的,有来自组织内的其他团队,也有来自组织外的其他团队。每个独立的部分都是一个仓库。值得注意的是,从最终产品的角度来看,每个团队的产出是一个仓库,但在团队内部,他们是工作空间。让我们看一个Gradle多项目构建示例(Android项目):.├──README.md├──library_a├──app│├──build.gradle│└──src├──build.gradle├──local.properties├──settings.gradle└──third-parties├──...├──build.gradle└──settings.gradle从目录结构来看,这是一个workspace,在workspace中嘛,它包括一些第三方代码仓库,以及自己的库library_a和应用app。所以这里的library_a和第三方的各个项目都是repository。包是代码的集合,可大可小。主要是因为在构建的时候,我们可能会从一个仓库(即使是最小的Gradle工程)生成多个包,比如Java工程中的src/main和src/test。所以在bazel等构建工具中,支持自定义包:src/my/app/BUILDsrc/my/app/app.ccsrc/my/app/data/input.txtsrc/my/app/tests/BUILDsrc/my/app/tests/test.cc对于一个包,我们往往需要定义一系列相关信息,比如包名、依赖信息、入口等。Bazel中Java构建的示例:java_binary(name="ProjectRunner",srcs=["src/main/java/com/phodal/ProjectRunner.java"],main_class="com.phodal.ProjectRunner",deps=[":greeter"],)这已经实现了对不同包的信息抽象。顺便看看Java包中的MANIFEST例子:Main-Class:HelloWorldClass-Path:libs/joda-time-2.10.6.jar,就可以知道它们之间的联系了。包定义在打包阶段,我们以一种简单的形式定义包——因为它不是那么重要,所以我们不关心。而当我们决定将这个包发布到互联网上时,我们需要很好地定义这个包。一些相应的必要信息是:nameversionauthorslicensedescription...这些信息用于显示在包管理中心,为用户提供包相关的信息。不同语言使用不同形式,Rust使用自定义toml,Maven仓库使用XML:
