构建这一问题,究竟是哪一个环节出了 Bug?程序员
我老是听到程序员谈论构建的问题:“构建出错了”,“我把构建搞坏了”等等。然而,真正的问题在于构建这个概念自己就有问题。每次修改应用程序都须要从头从新构建的想法从根本上就有缺陷。编程
这个概念的实际问题在于,构建会致使开发过程当中的反馈循环漫长而痛苦。有些系统的应对手段是经过极快的速度,他们的观点是,哪怕你每次进行修改都编译整个系统,也不是问题,由于在你反应过来以前构建就会结束。缓存
问题在于,系统的规模会一每天扩大,终有一天这个法子再也行不通。markdown
更深层次的问题是,你会发现每次修改代码后,即便当即从新构建,仍是须要从新启动应用程序,并且每当你发现一个问题、修改代码、通过从新编译后,问题仍是没有消失。换句话说,构建与实时编程是对立的。构建的反馈循环永远都那么漫长。机器学习
从根本上讲,每次修改代码的时候,不必从新编译整个系统。就好像你想换个灯泡也不必把整座摩天大楼都拆了重建吧。编程语言
不管怎么优化,构建都没法真正成为实时。函数
所以,诸如make之类的工具永远也没法解决问题。并且这些工具自己还有许多其余问题。例如,咱们必须复制代码中嵌入的依赖项信息:import、include、use或 extern 声明之类的语句为咱们提供的文件和模块信息与咱们手动输入并没有二样。这些复制工做乏味且容易出错。并且这种复制太粗糙,粒度仅限于文件。编译器能够更精确地管理这些依赖关系,例如跟踪何处使用了哪些函数。工具
注意:有些工具(如GN)能够由编译器提供依赖关系。虽然仍然很粗糙。oop
另外,这些工具提供的语言所具有的抽象机制和工具支持都不好。通常你们对make的不尽是其引入了其余相似性质的工具层,例如Cmake。性能
一个更好的解决方案是,为构建生成更好的DSL。基于某种真正的编程语言的内部DSL是改善问题的一种方法。例如 rake 和 scons,分别使用了 Ruby 和 Python。这些工具简化了构建工做,但它们仍与构建有着千丝万缕的联系,这才是我最关心的根本问题。
话虽如此,若是咱们不打算使用传统的构建系统来管理咱们的依赖项,那么应该怎么办?
首先,咱们须要认识到咱们的许多依赖关系不是基础的东西,例如可执行文件、共享库、目标文件和任意类型的二进制文件等。咱们真正须要“构建”的只有源代码。毕竟,若是使用解释器,那么你只需建立最基本的源代码,而后逐步编辑和发展源代码。
使用解释器能够避免构建二进制文件的问题。
成本是性能。编译是一种优化,尽管是很重要的优化,一般是必不可少的。编译须要比解释器更全面的分析,并且还会执行预先计算,以免咱们在执行过程当中重复工做。从某种意义上说,编译器的做用是记住解释器的部分工做。
许多动态JIT正是实现了这一点,但从根本上讲,静态编译也是如此——你只需提早记住便可。
从这个角度看,构建是分阶段执行的一种形式,而咱们不断构建的二进制文件只是缓存。
咱们能够经过结合解释与编译,解决解释器的性能难题。许多使用JIT编译器的系统正是这样作的。其优势之一在于,咱们没必要在启动应用程序以前等待优化。还有咱们能够修改代码,并经过从新解释和从新优化当即反应修改的结果。
然而,并不是全部的JIT都会这样作,可是这种作法已经延续了数十年,例如Smalltalk VM等。Smalltalk 有不少优势,其中之一即是你不多会遇到构建的麻烦。
然而,即便假设你的JIT引擎在发展的过程当中对代码进行了增量优化,你与实时开发之间仍有障碍,这个障碍就是你仍然须要构建。
即使咱们的资源可供随时使用,过分依赖文件系统结构也会引起问题。资源一般会以文件的形式呈现。部署的结构可能与源代码库不一样。在源代码库中编辑组件不会改变构建结构。
纠正这些问题并不是易事,一般软件工程师甚至都不肯意尝试。相反,他们会愈来愈依赖构建过程。这真的没有必要。
咱们能够将资源视为缓存的对象,并根据须要生成它们。部署应用程序时,咱们须要确保全部资源都通过了预先计算,并且缓存在应用程序的固定位置上,若是在开发过程当中这个缓存丢失,那么应用程序还能将它们放回同一个位置。软件知道这些位置,所以它可以找出缓存在应用程序固定位置上的资源。
当经过应用程序逻辑访问资源时,上述推理过程就很合理。那么那些没有被应用程序使用,而是提供给用户使用的资源呢?在有些状况下,文档、示例代码以及附带的资源都属于这种状况。这类资源的处理不是应用程序自己的一部分,所以,它不是构建问题,而是部署问题。也就是说,部署只是将合适的对象放到既定的位置上。
总结
让咱们建设一个全新、勇敢、没有构建的世界。