最近,我加入了一个名为 "每种编程语言的样本程序"的开源项目的核心团队。该项目的目标是提供一个不断增长的每种编程语言的简单程序集,以及解释每种实现的文章。我很快发现,我常用的语言大多是完整的,但我很高兴看到我正在学习的语言有机会,但我还没有机会开发一个完整的项目。
虽然我渴望有理由为其中一些语言建立一个完整的开发环境,但单文件项目似乎不是这个理由,而我的极简主义Arch Linux设置也同意我的看法。尽管如此,我仍然需要为这些语言中的每一种至少安装一个编译器,但我对机器杂乱无章的恐惧使我不敢随便安装一堆编译器/磁盘。
要求
我希望能够在以下要求下执行我的开发周期。- 不在我的机器上添加软件包来构建代码 - 不留下任何构建工件、依赖关系和其他缓存项目 - 除了项目本身的源代码外,不添加任何文件或文件夹 - 在清理隔离的构建/调试/运行空间后坚持代码修改 - 在开发期间持续运行和调试代码
解决方案。Docker
我首先想到的是Docker,它为我提供了这种隔离环境。使用Docker,我可以快速建立一个干净的环境来构建和运行我的代码,而且我也可以很容易地将其拆除。
Docker工作流程
由于我对这个练习的要求之一是避免添加额外的文件或留下人工制品,我选择避免Dockerfiles 和任何类型的本地docker build 。我决定将我的工作流程建立在docker run 。对于haskell,我想出了以下办法。
docker run -it --rm -v $(pwd):/haskell -w /haskell haskell /bin/bash
让我们把它分成几个部分。
docker run -it --rm ... haskell ...
交互式地运行来自Docker Hub的hashkell的最新官方镜像。当容器停止时,移除它。
... -v $(pwd):/haskell ...
将当前目录与容器内的/haskell 目录绑定。这个目录中的任何东西现在都存在于容器中。这意味着我们可以从容器中构建、打包、修改等......任何在当前目录中找到的源代码。注意,这是一个绑定的挂载,而不是一个拷贝,所以对容器内的文件和文件夹的任何修改也会在容器外发生,反之亦然。
... -w /haskell ...
将容器的工作目录设置为/haskell
... bash -c "cd /haskell && exec /bin/bash"
这是在容器启动后立即给它的命令。基本上,这将在/haskell 目录中给你一个bash提示。
所以总的来说,你得到的是一个隔离环境中的bash shell提示符,这个环境只包含你的源代码和用于构建和运行它的工具。你可以使用这个环境与你的源代码进行交互,就像你在一个新建立的开发环境中那样。在退出这个环境后,除了/haskell ,所有的东西都会被自动清理掉。
通用的Docker工作流功能
在为上面的docker run 命令创建了几个几乎相同的别名后,我想出了一个更通用的函数。
function containerhere {
[[ -z $1 ]] && { echo "usage: containerhere IMAGE [COMMAND]"; return 1; }
command=$2
if [[ -z $command ]]; then
command='/bin/bash'
fi
bash -c "docker run --rm -it -v $(pwd):/data -w /data $1 $command"
}
这个函数基本上与前面的例子相同,但需要你指定一个Docker镜像来运行。它还允许你选择性地覆盖启动时传递给容器的命令。
你可以通过调用containerhere haskell ,达到与前面例子相同的结果。需要注意的一个区别是,当前目录被绑定挂载到/data ,而不是/haskell 。
Docker:优势
这种方法的一些优点包括以下几点。- 环境是完全隔离的。任何未写入绑定的/haskell 目录的缓存、软件包安装或构建工件都会被自动清理。- 每一次docker run ,环境都以干净的状态开始 - 当前目录和/haskell 目录之间的本地绑定挂载允许你从容器内部或外部进行修改。这意味着你仍然可以使用你最喜欢的文本编辑器在你的主机上编写代码,并在容器内构建/运行/测试它。
Docker:局限性
每种方法都有其利弊。以下是我想到的一些限制。- 持久性。只有在当前目录绑定挂载中的变化才会被持久化。这意味着你可能需要在每个新的docker run ,重新下载和/或重新安装依赖。这个问题是有解决办法的,但不在这篇文章的范围之内。- 调试。这种方法并不能提供完整的IDE调试体验。因为SDK/编译器存在于容器中,你的IDE不能自动访问它。你可能希望研究一下你的语言的命令行调试选项,如python提供的调试选项。
真实世界的例子
我将首先为我的项目创建一个新的目录,并在该目录下创建一个main.hs文件
$ mkdir ~/code/quicksort
$ cd ~/code/quicksort
$ touch main.hs
我将把我的代码(如下)添加到main.hs中。
module Main where
import System.Random (randomRIO)
quicksort :: Ord a => [a] -> [a]
quicksort [] = []
quicksort (p:xs) = (quicksort lesser) ++ [p] ++ (quicksort greater)
where
lesser = filter (< p) xs
greater = filter (>= p) xs
randomList :: Int -> IO([Int])
randomList 0 = return []
randomList n = do
r <- randomRIO (1,1000)
rs <- randomList (n-1)
return (r:rs)
main :: IO ()
main = do
random25 <- randomList 25
print $ quicksort random25
我不会花时间解释这段Haskell代码,只是说当执行时,它将生成一个1到1000之间的25个随机数的列表,然后用快速排序法进行排序。
在这一点上,我将启动我的Haskell环境。
$ containerhere haskell
而我将收到类似的提示。 [[email protected]](https://www.pluralsight.com/cdn-cgi/l/email-protection):/data#
如果我列出当前目录的内容,我将看到我的main.hs。
[email protected]:/data# ls
main.hs
如果我想交互式地测试我的代码,我可以调出ghci,Haskell的交互式REPL,并加载main.hs
[email protected]:/data# ghci
GHCi, version 8.6.3: http://www.haskell.org/ghc/ :? for help
Prelude> :l main.hs
[1 of 1] Compiling Main ( main.hs, interpreted )
main.hs:3:1: error:
Could not find module ‘System.Random’
Use -v to see a list of the files searched for.
|
3 | import System.Random (randomRIO)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Failed, no modules loaded.
我收到一个错误,告诉我System.Random没有安装在我的环境中。我用:q 退出ghci,然后用cabal,一个Haskell软件包管理器进行安装。
Prelude> :q
Leaving GHCi.
[email protected]:/data# cabal update
[email protected]:/data# cabal install random
...
注意:我省略了 cabal 下载和安装random 模块的一些输出"
现在我可以尝试再次将main.hs 加载到ghci中。
[email protected]:/data# ghci
GHCi, version 8.6.3: http://www.haskell.org/ghc/ :? for help
Prelude> :l main.hs
[1 of 1] Compiling Main ( main.hs, interpreted )
Ok, one module loaded.
而且我可以通过调用以下命令来运行它:main
*Main> :main
[65,110,129,191,258,272,287,352,394,452,458,473,473,491,501,564,570,590,726,760,795,830,869,882,908]
在这一点上,我可以通过在容器内安装CLI文本编辑器或从容器外进行修改来对我的源代码进行任何我喜欢的修改。我通常保持两个终端窗口打开。一个是在容器外面,是一个vim窗口,我在那里修改我的源代码。另一个是我运行containerhere 的地方,我用它来不断地加载和运行我的代码。
如果我需要构建我的代码,我也可以通过调用Haskell编译器ghc来完成。
[email protected]:/data# ghc main.hs
[1 of 1] Compiling Main ( main.hs, main.o )
Linking main ...
这将给我一个编译的二进制文件。
[email protected]:/data# ls
main main.hi main.hs main.o
这些文件可以从容器内部运行
[email protected]:/data# ./main
[81,125,136,144,165,216,221,224,234,235,254,271,273,476,572,610,616,623,656,659,808,848,931,933,1000]
或从容器外运行
$ ./main
[122,178,180,204,230,265,266,331,334,413,428,452,453,564,564,622,631,633,658,659,674,685,912,965,976]
总结
在这篇文章中,我描述了我对一个特定问题的解决方案。然而,我相信这种模式可以在大量的情况下使用。它是开始学习一门新语言的一个伟大的、低投入的方法。当我使用我自己的电脑以外的电脑时,例如在与我的团队结对子或打交道时,它也能发挥作用。我发现它在任何时候都很有用,我迅速想要一个干净的、孤立的环境,可以不假思索地消失。希望不管你的工作流程如何,只要你需要一个临时的开发环境,这种方法就能帮助你。