真香,我用Makefile做Flutter项目的自动化管理

9,361 阅读7分钟

在开发我们的 Flutter 项目时,有许多可重复的任务——格式化、在我们创建 PR 之前运行单元测试、清理项目,以及运行甚至构建不同风格的应用程序。

尽管我们的 IDE 可以轻松执行其中一些操作,但我们可能已经爱上了命令行,这意味着我们要么创建我们的脚本,要么我们每次都必须手动输入每个命令。

那有没有更好的方法来自动化这个?

有!那就是Makefile

Makefile允许我们在一个文件中创建一组不同的命令来自动化我们的工作流程。有了它,我们可以同时做三件事:

  1. 在同一个地方我们可以定义项目中所需的所有操作;
  2. 使用简短的命令可以更轻松地执行这些操作;
  3. 我们可以在所以的 Flutter 项目中使用此工具;

是不是很香,但让我们从头开始创建Makefile文件吧。

什么是Makefile?

GNU 手册上make清晰的支出了 Makefile 的目的:

The make utility automatically determines which pieces of a large program need to be recompiled, and issues commands to recompile them. (…) make is not limited to programs. You can use it to describe any task where some files must be updated automatically from others whenever the others change.。

在 Flutter 或 Dart 开发中,我们可以使用它来创建和执行任务。

想象一下——当每次我们运行或打包 iOS 和 Android 程序时,我们都需要手动执行以下步骤:

  1. 清理项目
  2. 运行 lint 以查看我们是否没有任何错误
  3. 运行所有测试
  4. 代码风格格式化
  5. 分发我们的应用

手动去做这些过程,会有两个问题:

  1. 每次需要执行新版本时,我们都需要手动运行每个命令,并且要保证每个步骤都没问题。2. 如果每次都手动去做,会耗费很大的精力,所以效率是第一生产力。

使用 makefile,我们可以简化这些操作,将这些操作分解为简单的任务,称为targets,并将它们关联在一起,最终我们就痛可以通过调用make build_mobile_stg来自动完成以上所有步骤.

创建一个基本的 Makefile

首先, 在我们项目的根目录创建一个名称为Makefile(没有扩展名)的文件。我们现在``Makefile中加入targetsvariables

  • targets- 声明被调用时要运行的命令是什么。它们可以设置 precedent target,也就是说我们可以将这些关联命令在一起,如build_stg只会在run_unit_tests成功后调用,run_unit_tests将在lint`成功后调用。
  • variables- 在每个target内使用。可以用来指定build目录或这当前目录先做一些命令。

target的定义如下

target_name: precedent_target_one precedent_target_two
	command_1
	command_2

Makefile代码时缩进 只允许 制表符

下面我们开始Makefile在flutter中实战。

首先创建一个可以使用dart format用来格式化代码:

format:
    dart format .

现在我们可以简单地运行make format,这样所有的代码都将被格式化:

➜  flutter_makefiles git:(master) ✗ make format
dart format .
Formatted 2 files (0 changed) in 0.28 seconds.

输入命令后我们在终端中可以看到两个输出: 一个我们用来格式化代码的命令dart format .和格式化的结果。 如果我们不想在终端中看到我们的命令,可在Makefile中加一个@

format:
   @dart format .

现在make只会显示如下的输出:

➜  flutter_makefiles git:(master) ✗ make format
Formatted 2 files (0 changed) in 0.27 seconds.

为了输出更清晰target的操作,可以 添加一个简短的提示echo,以便知道当前进行的步骤。

format:
    @echo "╠ Formatting the code"
    @dart format .

输出为:

➜  flutter_makefiles git:(master) ✗ make format
╠ Formatting the code
Formatted 2 files (0 changed) in 0.22 seconds.

创建新规则

前面是一个format任务,现在我们再创建其他的命令。

clean: 
    @echo "╠ Cleaning the project..."
    @rm -rf pubspec.lock
    @flutter clean

format:
    @echo "╠ Formatting the code"
    @dart format .

现在我们可以在终端通过make来调用不同的target了:

但是,Makefile默认情况下是用来_创建新文件_。由于我们只是在_运行命令_,我们应该告诉Makefile不会生成与target名称相同的文件。

这样我们就需要在文件顶部添加了一个.PHONY标志,在其中声明所有_不_生成同名文件的target。要了解更多关于phony target在其中声明所有_不_生成同名文件的,您可以查看GNU手册phony target

.PHONY: clean format

clean: 
    @echo "╠ Cleaning the project..."
    @rm -rf pubspec.lock
    @flutter clean

format:
    @echo "╠ Formatting the code"
    @dart format .

我们可以安全地运行make cleanmake format来看看是不是达到了效果。

关联规则

既然我们知道如何创建新命令,我们如何关联命令?

假设我们要创建的是:

可以设置命令执行的优先级:

  .PHONY: clean format upgrade 

# Other targets

upgrade: clean 
    @echo "╠ Upgrading dependencies..."
    @flutter pub upgrade

如果我们现在运行make upgrade发现在控制台中看到两个target都被调用:

➜  flutter_makefiles git:(master) ✗ make upgrade       
╠ Cleaning the project...
Cleaning Xcode workspace...                                         3.1s
Cleaning Xcode workspace...                                         3.9s
Deleting .dart_tool...                                               1ms
Deleting .packages...                                                0ms
Deleting Generated.xcconfig...                                       0ms
Deleting flutter_export_environment.sh...                            0ms
Deleting ephemeral...                                                0ms
╠ Upgrading dependencies...
Resolving dependencies...

但是如果target 中的一个优先级高的失败会发生什么?假设我们要添加两个目标:

  1. run_unit - 将运行所有单元测试
  2. build_dev_mobile- 在所有测试运行并通过后,我们构建dev版的应用程序。

让我们首先将这些目标添加到Makefile

.PHONY: clean format upgrade build_dev_mobile run_unit

# Other targets

run_unit: 
    @echo "╠ Running the tests"
    @flutter test

build_dev_mobile: clean run_unit
    @echo "╠ Building the app"
    @flutter build apk --flavor dev

如果测试运行失败,我们将看到以下消息:

➜  flutter_makefiles git:(master) ✗ make build_dev_mobile
╠ Cleaning the project...
# ...
╠ Running the tests
Running "flutter pub get" in flutter_makefiles...                2,177ms
# ...
00:09 +0 -1: Counter increments smoke test [E]                                                                                       
  Test failed. See exception logs above.
  The test description was: Counter increments smoke test
  
00:09 +0 -1: Some tests failed.                                                                                                      
make: *** [run_unit] Error 1

如我们所见,该make命令将在build_dev_mobile执行之前失败。但是错误信息不够清楚:make: *** [run_unit] Error 1.

当命令失败时,我们可以使用||运算符提供更合适的错误消息:

run_unit: 
    @echo "╠ Running the tests"
    @flutter test || (echo "▓▓ Error while running tests ▓▓"; exit 1)

现在我们重新运行make build_dev_mobile,我们会看到自定义的错误消息:

➜  flutter_makefiles git:(master) ✗ make build_dev_mobile
╠ Cleaning the project...
# ...
╠ Running the tests
# ...  
00:08 +0 -1: Some tests failed.                                                                                                      
▓▓ Error while running tests ▓▓
make: *** [run_unit] Error 1

使用“all”创建默认target

我们已经定义了很多phony targets,但是如果我们只调用make不加参数会发生什么?

默认情况下,它只会运行第一个target。但这满足我们的需求。我们需要关联多个动作,例如:

  1. 对代码进行 lint 分析—— lint
  2. 格式化代码—— format
  3. 运行dev -run_dev_mobile

但是,我们不想在run_dev_mobile预先定义好所有这些操作。

我们先增加两个新的target:

run_dev_mobile: 
    @echo "╠ Running the app"
    @flutter run --flavor dev

lint: 
    @echo "╠ Verifying code..."
    @dart analyze . || (echo "▓▓ Lint error ▓▓"; exit 1)

接下来我们要按顺序执行lintformatrun_dev_mobile。为了每次我们使用make就可以按顺序执行,我们需要加入 all 参数;

.PHONY: all run_dev_mobile run_unit clean upgrade lint format

all: lint format run_dev_mobile

# All other targets

我们都会看到以下输出:

➜  flutter_makefiles git:(master) ✗ make
╠ Verifying code...
Analyzing ....                         1.2s
No issues found!
╠ Formatting the code
Formatted 2 files (0 changed) in 0.33 seconds.
╠ Running the app
# ...

添加帮助命令

虽然没有现成的显示帮助消息的解决方案,但我们可以使用这个非常有用的Github Gist

按照约定,我们可以在每个target后添加注释,以便我们可以显示帮助消息:

.PHONY: all run_dev_web run_dev_mobile run_unit clean upgrade lint format help

all: lint format run_dev_mobile

# Adding a help file: https://gist.github.com/prwhite/8168133#gistcomment-1313022
help: ## This help dialog.
    @IFS=$$'\n' ; \
    help_lines=(`fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//'`); \
    for help_line in $${help_lines[@]}; do \
        IFS=$$'#' ; \
        help_split=($$help_line) ; \
        help_command=`echo $${help_split[0]} | sed -e 's/^ *//' -e 's/ *$$//'` ; \
        help_info=`echo $${help_split[2]} | sed -e 's/^ *//' -e 's/ *$$//'` ; \
        printf "%-30s %s\n" $$help_command $$help_info ; \
    done

run_dev_mobile: ## Runs the mobile application in dev
    @echo "╠ Running the app"
    @flutter run --flavor dev

run_unit: ## Runs unit tests
    @echo "╠ Running the tests"
    @flutter test || (echo "Error while running tests"; exit 1)

# Remaining targets

运行make help会输出:

➜  flutter_makefiles git:(master) ✗ make help
help:                          This help dialog.
run_dev_mobile:                Runs the mobile application in dev
run_unit:                      Runs unit tests
clean:                         Cleans the environment
format:                        Formats the code
lint:                          Lints the code
upgrade: clean                 Upgrades dependencies

总结

如果使用得当,它Makefile可以为我们提供灵活性和速度。提高工作效率的唯一限制是我们的创造力。

现在我们来看看可以用Makefile做哪些事情:

  • 自动打包构建上传应用分发;
  • build_runner自动生成新文件
  • 提交代码前自动格式化和运行测试
  • 使用flavordart-define将新配置注入到应用程序的不同构建和运行中,无论是开发、暂存、生产还是不同的品牌。

它的另一个好处是可以通用——这意味着我们可以将它从一个项目带到另一个项目,并将其作为我们应用程序开发的主要工具集。

需要注意的是,我们在每个target内使用制表符而不是空格是非常重要的。复制和粘贴内容时尤其如此。如果我们添加空格而不是制表符,我们将收到以下错误消息:

Makefile:34 *** missing separator. Stop.

最后祝大家工作愉快,不用加班