当前位置: 首页 > 科技观察

说说构建的抽象

时间:2023-03-18 18:55:54 科技观察

最近在研究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:.....................类似NPM中也使用类似的字段package.json:名称、版本号等信息。在这些编程语言中,这个东西被设计的太简单了,比如Python的pip里面用requirements.txt来管理依赖,setup.py用来配置什么时候放包。所以,如果你的应用没有发布,就没有包名。。。包布局构建工具在设计的时候,会设计默认的软件包层次结构,这个层次结构就是包布局(packagelayout).构建工具使用此布局来获取所需的输入源和配置信息。它还包含了一些默认的配置,比如src/main指向源码目录,src/test指向测试代码(产品中不会加入)├──build.gradle└──src├──main└──test用户也可以根据需要扩展这个布局,比如Gradle中的SourceSets:sourceSets{main{output.resourcesDir=file('out/bin')java.outputDir=file('out/bin')}}与其他语言类似。但是,对于某些语言来说,并没有这么强的关联,比如在Golang中,就没有这么强的约束。只是本来是默认值,现在需要开发者手动配置。工件工件是最终的构建工件。同样,不同的语言有不同的命名约定。在Gradle中称为工件,在Rust中称为目标……。产品,主要涉及各种文件的流转及其流转规则。举个简单的例子,一个jar文件中必须包含一个MANIFEST.MF,用于配置应用、扩展、类加载器等相关信息。相关文件将以META-INF的形式组织。所以,在创建整个产品的过程中,就是复制相应的文件,进行相应的转换,比如java->.class,复制到相应的目录,最后打包在一起的过程。任务:规则引擎+DSL在上面我们看到的例子中,很多都创建了自己的DSL,然后用它们来构建。只有这样,用户才能得到最大的便利。这是一个相当复杂的过程,相当于设计一个独立于平台和语言的DSL。这会以多种方式发展:具有API抽象的内部DSL。Webpack、Gulp等实现Homebrew外部DSL语言。比如Gradle和Bazel在多种语言中使用的Groovy。规则引擎本身是一组关于任务的DSL,参见Gradle示例:taskcopyReportsDirForArchiving2(type:Copy){from("$buildDir"){include"reports/**"}into"$buildDir/toArchive"}它确实是复制。对应的Gradle打包示例也是一个非常简单的DSL抽象:taskpackageDistribution(type:Zip){archiveFileName="my-distribution.zip"destinationDirectory=file("$buildDir/dist")from"$buildDir/toArchive"}使用Gradle是一个外部DSL。看一下Webpack的打包示例:module.exports={entry:'./path/to/my/entry/file.js',output:{filename:'my-first-webpack.bundle.js',路径:path.resolve(__dirname,'dist')},module:{rules:[{test:/\.(js|jsx)$/,use:'babel-loader'}]},plugins:[newwebpack.ProgressPlugin(),newHtmlWebpackPlugin({template:'./src/index.html'})]};这里的规则是一个简单的规则引擎(使用正则表达式来匹配)。两种模式各有优缺点,复杂的场景下,使用DSL+自定义脚本更容易。PS:看来我有空了,我也应该写一个规则引擎构建的扩展对于主流的构建系统,它们都支持不同形式的扩展支持:外部DSL扩展插件接口编程项目项目外编程语言扩展大部分的编程语言扩展在文章的前面部分已经提到过,这里不再赘述。结束语构建应用程序是一个相当有趣的过程。设计构建系统也变得非常有趣。参考:GradlevsBazelforJVMProjectsBazel:概念和术语Yarn:WorkspacesGradle:创作多项目构建Cargo:WorkspacesGulp:Tasks用于相关目的的开源库:lerna用于管理具有多个包的JavaScript项目的工具。bazelBlueprint是一个元构建系统,它读取描述需要构建的模块的蓝图文件,并生成描述需要运行的命令及其依赖项的Ninja清单。二维码关注。转载本文请联系phodal公众号。