Makefile教程1 快速入门

这篇具有很好参考价值的文章主要介绍了Makefile教程1 快速入门。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1 快速入门

1.1 为什么存在 Makefile?

Makefile用于帮助决定大型程序的哪些部分需要重新编译。在绝大多数情况下,都会编译C或C++文件。 其他语言通常有自己的工具,其用途与Make类似。当您需要根据已更改的文件运行一系列指令时,Make也可以在编译之外使用。 本教程将重点介绍C/C++编译。

下面是您可以使用Make构建的示例依赖关系图。如果任何文件的依赖项发生更改,则该文件将被重新编译:

Makefile教程1 快速入门

1.2 Make有哪些替代?

流行的C/C++替代构建系统有SCons、CMake、Bazel 和 Ninja。 一些代码编辑器(例如 Microsoft Visual Studio)有自己的内置构建工具。 对于Java,有Ant、Maven和Gradle。 其他语言(例如 Go、Rust 和 TypeScript)都有自己的构建工具。

Python、Ruby 和Javascript等解释性语言不需要与Makefile之类的东西。 Makefile的目标是根据已更改的文件来编译需要编译的任何文件。 解释语言中的文件发生更改时,不需要重新编译任何内容。 程序运行时,将使用该文件的最新版本。

1.3 Make的版本和类型

Make有多种实现,但本指南的大部分内容都适用于您使用的任何版本。 然而,它是专门为GNU Make编写的,GNU Make是Linux和MacOS上的标准实现。 所有示例都适用于Make版本3和4,除了一些个别差异之外,它们几乎相同。

1.4 运行示例

要运行这些示例,您需要一个终端并安装“make”。 对于每个示例,将内容放入名为Makefile的文件中,然后在该目录中运行命令make。 让我们从最简单的Makefile开始:

hello:
	echo "Hello, World"

注意:Makefile必须使用TAB缩进,不能使用空格,否则make将失败。

以下是运行上述示例的输出:

$ make
echo "Hello, World"
Hello, World

1.5 Makefile语法

生成文件语法
Makefile 由一组规则组成。 规则通常如下所示:

targets: prerequisites
	command
	command
	command
  • targets是文件名,以空格分隔。 通常每条规则只有一个。
  • 这些command是通常用于创建目标的一系列步骤。
  • prerequisites 也是文件名,以空格分隔。 在运行target的命令之前,这些文件需要存在。 这些也称为依赖项

1.6 Make的本质

让我们从hello world示例开始:

hello:
	echo "Hello, World"
	echo "This line will print if the file hello does not exist."
  • 我们有一个名为 hello 的目标
  • 该目标有两个命令
  • 该目标没有先决条件

然后我们将运行 make hello。 只要hello文件不存在,命令就会运行。 如果hello存在,则不会运行任何命令。

让我们创建更典型的Makefile:编译单个C文件。

blah.c

int main() { return 0; }

然后创建 Makefile(一如既往地称为 Makefile):

blah:
	cc blah.c -o blah

运行make,由于没有将目标作为参数提供给make命令,因此将运行第一个目标。 在这种情况下,只有一个目标(blah)。 第一次运行它时,将会创建blah。 第二次,你会看到“make: 'blah' is up to date”。 那是因为blah文件已经存在。 但有一个问题:如果我们修改blah.c 然后运行make,则不会重新编译任何内容。

我们通过添加先决条件来解决这个问题:

blah: blah.c
	cc blah.c -o blah

当我们再次运行make 时,会发生以下步骤:

  • 选择第一个目标,因为第一个目标是默认目标
  • 这有blah.c的先决条件
  • Make决定是否应该运行blah目标。 仅当blah不存在或blah.c比 blah新时才会运行

最后一步很关键,也是make的精髓。它试图做的是确定自上次编译blah以来blah的先决条件是否发生了变化。也就是说,如果blah.c被修改,运行make应该重新编译该文件。 相反,如果blah.c没有更改,则不应重新编译它。

为了实现这一点,它使用文件系统时间戳来确定是否发生了更改。 这是一个合理的启发式方法,因为文件时间戳通常仅在文件被修改时才会更改。 但情况并非总是如此。 例如,您可以修改文件,然后将该文件的修改时间戳更改为旧的时间戳。 如果这样做,Make会错误地猜测该文件没有更改,因此可以被忽略。

唷,真是拗口啊。 确保您理解这一点。 这是 Makefile 的关键,您可能需要几分钟才能正确理解。 如果事情仍然令人困惑,请尝试上面的示例或观看上面的视频。

参考资料

  • 软件测试精品书籍文档下载持续更新 https://github.com/china-testing/python-testing-examples 请点赞,谢谢!
  • 本文涉及的python测试开发库 谢谢点赞! https://github.com/china-testing/python_cn_resouce
  • python精品书籍下载 https://github.com/china-testing/python_cn_resouce/blob/main/python_good_books.md
  • Linux精品书籍下载 https://www.cnblogs.com/testing-/p/17438558.html
  • https://makefiletutorial.com/

1.6 更多示例

以下Makefile最终运行所有三个目标。 当您在终端中运行make时,它将通过一系列步骤构建名为blah的程序:

  • Make选择目标blah,因为第一个目标是默认目标
  • blah需要blah.o,因此搜索blah.o目标
  • blah.o需要blah.c,因此搜索blah.c目标
  • blah.c 没有依赖项,因此运行echo命令
  • 然后运行 cc -c 命令,因为所有blah.o依赖项都已完成
  • 运行顶部cc命令,因为所有blah依赖都完成了
  • 最终:blah是已编译的c程序
blah: blah.o
	cc blah.o -o blah # Runs third

blah.o: blah.c
	cc -c blah.c -o blah.o # Runs second

# Typically blah.c would already exist, but I want to limit any additional required files
blah.c:
	echo "int main() { return 0; }" > blah.c # Runs first

如果删除blah.c,所有三个目标都将重新运行。如果您运行touch blah.o (从而将时间戳更改为比 blah 更新),则只有第一个目标会运行。如果您不进行任何更改,则所有目标都不会运行。

下一个示例没有做任何新内容,但仍然很好的补充示例。它将始终运行两个目标,因为some_file依赖于other_file,而other_file从未创建。

some_file: other_file
	echo "This will always run, and runs second"
	touch some_file

other_file:
	echo "This will always run, and runs first"

1.7 Make clean

clean经常被用作删除其他目标输出的目标,你可以运行make和make clean来创建和删除some_file。

clean在这里做了两件新事情:

  • 它不是第一目标(默认),也不是先决条件。这意味着除非你明确调用 make clean,否则它永远不会运行。
  • 它不是一个文件名。如果你碰巧有一个名为clean的文件,这个目标就不会运行,这不是我们想要的。
some_file: 
	touch some_file

clean:
	rm -f some_file

1.8 变量

变量只能是字符串。通常要使用 :=,但 = 也可以。

下面是一个使用变量的示例:

files := file1 file2
some_file: $(files)
	echo "Look at this variable: " $(files)
	touch some_file

file1:
	touch file1
file2:
	touch file2

clean:
	rm -f file1 file2 some_file

单引号或双引号对Make没有任何意义。它们只是分配给变量的字符。不过,引号对shell/bash很有用,在printf等命令中需要用到它们。在本例中,两个命令的行为是一样的:

a := one two # a is set to the string "one two"
b := 'one two' # Not recommended. b is set to the string "'one two'"
all:
	printf '$a'
	printf $b

使用 ${} 或 $() 引用变量

x := dude

all:
	echo $(x)
	echo ${x}

	# Bad practice, but works
	echo $x 

2 Target

2.1 all target

如果调用 make 而未指定目标,它将默认运行。

all: one two three

one:
	touch one
two:
	touch two
three:
	touch three

clean:
	rm -f one two three

2.2 多target

当一条规则有多个目标时,将针对每个目标运行命令。$@ 是一个自动变量,包含目标名称。

all: f1.o f2.o

f1.o f2.o:
	echo $@
# Equivalent to:
# f1.o:
#	 echo f1.o
# f2.o:
#	 echo f2.o

3 自动变量和通配符

3.1 * 通配符

在 Make 中,* 和 % 都被称为通配符,但它们的含义完全不同。* 在文件系统中搜索匹配的文件名。我建议你始终将其封装在通配符函数中,否则你可能会陷入下面描述的一个常见陷阱。

# Print out file information about every .c file
print: $(wildcard *.c)
	ls -la  $?

*可以在目标文件、先决条件或通配符函数中使用。

不要在变量定义中直接使用 *。当 * 不能匹配任何文件时,将保持原样(除非在通配符函数中运行)

thing_wrong := *.o # Don't do this! '*' will not get expanded
thing_right := $(wildcard *.o)

all: one two three four

# Fails, because $(thing_wrong) is the string "*.o"
one: $(thing_wrong)

# Stays as *.o if there are no files that match this pattern :(
two: *.o 

# Works as you would expect! In this case, it does nothing.
three: $(thing_right)

# Same as rule three
four: $(wildcard *.o)

3.2 %通配符

%非常有用,但由于其使用场合多种多样,所以有点令人困惑。

  • 在匹配模式下使用时,它会匹配字符串中的一个或多个字符。这种匹配称为词干。
  • 在替换模式下使用时,它会将匹配到的词干替换到字符串中。
  • % 通常用于规则定义和某些特定函数中。

3.3 自动变量

自动变量有很多,但通常只显示几个:

hey: one two
	# Outputs "hey", since this is the target name
	echo $@

	# Outputs all prerequisites newer than the target
	echo $?

	# Outputs all prerequisites
	echo $^

	touch hey

one:
	touch one

two:
	touch two

clean:
	rm -f hey one two
CC = gcc # Flag for implicit rules
CFLAGS = -g # Flag for implicit rules. Turn on debug info

# Implicit rule #1: blah is built via the C linker implicit rule
# Implicit rule #2: blah.o is built via the C compilation implicit rule, because blah.c exists
blah: blah.o

blah.c:
	echo "int main() { return 0; }" > blah.c

clean:
	rm -f blah*
	

4.2 静态模式规则

静态模式规则是在Makefile中少写代码的另一种方法,但我认为它更有用,而且不那么"神奇"。下面是它们的语法:

targets...: target-pattern: prereq-patterns ...
   commands

其本质是,给定的目标与 target-pattern(通过%通配符)匹配。匹配到的内容称为词干。然后将词干代入先决条件模式,生成目标的先决条件。

一个典型的用例是将 .c 文件编译成 .o 文件。下面是手动方法:

objects = foo.o bar.o all.o
all: $(objects)

# These files compile via implicit rules
foo.o: foo.c
bar.o: bar.c
all.o: all.c

all.c:
	echo "int main() { return 0; }" > all.c

%.c:
	touch $@

clean:
	rm -f *.c *.o all

下面是使用静态模式规则的更有效方法:

obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c

all: $(obj_files)
# Note: PHONY is important here. Without it, implicit rules will try to build the executable "all", since the prereqs are ".o" files.
.PHONY: all 

# Ex 1: .o files depend on .c files. Though we don't actually make the .o file.
$(filter %.o,$(obj_files)): %.o: %.c
	echo "target: $@ prereq: $<"

# Ex 2: .result files depend on .raw files. Though we don't actually make the .result file.
$(filter %.result,$(obj_files)): %.result: %.raw
	echo "target: $@ prereq: $<" 

%.c %.raw:
	touch $@

clean:
	rm -f $(src_files)

4.3 静态模式规则和过滤器

过滤器函数可用于静态模式规则,以匹配正确的文件。在本例中,我创建了 .raw 和 .result 扩展名。

obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c

all: $(obj_files)
# Note: PHONY is important here. Without it, implicit rules will try to build the executable "all", since the prereqs are ".o" files.
.PHONY: all 

# Ex 1: .o files depend on .c files. Though we don't actually make the .o file.
$(filter %.o,$(obj_files)): %.o: %.c
	echo "target: $@ prereq: $<"

# Ex 2: .result files depend on .raw files. Though we don't actually make the .result file.
$(filter %.result,$(obj_files)): %.result: %.raw
	echo "target: $@ prereq: $<" 

%.c %.raw:
	touch $@

clean:
	rm -f $(src_files)

4.4 模式规则

模式规则经常被使用,但很容易混淆。你可以从两个方面来看待它们:

  • 定义自己的隐式规则
  • 更简单的静态模式规则

我们先来看一个例子:

# Define a pattern rule that compiles every .c file into a .o file
%.o : %.c
		$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

模式规则的目标中包含一个"%"。这个"%"匹配任何非空字符串,而其他字符则自行匹配。模式规则先决条件中的"%"代表与目标中的"%"匹配的同一词干。

下面是另一个例子:

# Define a pattern rule that has no pattern in the prerequisites.
# This just creates empty .c files when needed.
%.c:
   touch $@

4.5 双冒号规则

双冒号规则很少使用,但可以为同一目标定义多个规则。如果这些规则是单冒号,则会打印警告,并且只会运行第二组命令。

all: blah

blah::
	echo "hello"

blah::
	echo "hello again"

5 命令和执行

5.1 命令回显/静默

在命令前添加 @ 以阻止命令被打印
make -s相当于在每一行前添加 @ 号

all: 
	@echo "This make line will not be printed"
	echo "But this will"

5.2命令执行

每条命令都在一个新的 shell 中运行(至少效果是这样的)

all: 
	cd ..
	# The cd above does not affect this line, because each command is effectively run in a new shell
	echo `pwd`

	# This cd command affects the next because they are on the same line
	cd ..;echo `pwd`

	# Same as above
	cd ..; \
	echo `pwd`

5.3默认shell

默认shell 是/bin/sh。你可以通过修改变量SHELL来改变它:

SHELL=/bin/bash

cool:
	echo "Hello from bash"

5.4双美元符号

如果想让字符串带有美元符号,可以使用 $$。这就是如何在 bash 或 sh 中使用 shell 变量。

请注意下一个示例中 Makefile 变量与 Shell 变量的区别。

make_var = I am a make variable
all:
	# Same as running "sh_var='I am a shell variable'; echo $sh_var" in the shell
	sh_var='I am a shell variable'; echo $$sh_var

	# Same as running "echo I am a make variable" in the shell
	echo $(make_var)

5.5 使用 -k、-i 和 - 处理错误

运行 make 时添加 -k,即使出现错误也能继续运行。如果你想一次性看到 Make 的所有错误,这很有用。
在命令前添加 - 来抑制错误
在 make 中添加 -i 可让每条命令都出现这种情况。

one:
	# This error will be printed but ignored, and make will continue to run
	-false
	touch one

5.6中断或杀死 make

注意:如果你按住 ctrl+c make,它会删除刚刚生成的新目标。

5.7递归使用 make

要递归调用 makefile,请使用特殊的 $(MAKE) 代替 make,因为它会为你传递 make 标志,而自身不会受其影响。

new_contents = "hello:\n\ttouch inside_file"
all:
	mkdir -p subdir
	printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
	cd subdir && $(MAKE)

clean:
	rm -rf subdir

5.7导出、环境和递归 make

当 Make 启动时,它会自动从执行时设置的所有环境变量中创建 Make 变量。

# Run this with "export shell_env_var='I am an environment variable'; make"
all:
	# Print out the Shell variable
	echo $$shell_env_var

	# Print out the Make variable
	echo $(shell_env_var)

export 指令将一个变量设置为所有配方中所有 shell 命令的环境:

shell_env_var=Shell env var, created inside of Make
export shell_env_var
all:
	echo $(shell_env_var)
	echo $$shell_env_var

因此,当你在 make 中运行 make 命令时,可以使用 export 指令使子 make 命令可以访问该变量。在本例中,cooly 被导出,以便子目录中的 makefile 可以使用它。

new_contents = "hello:\n\techo \$$(cooly)"

all:
	mkdir -p subdir
	printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
	@echo "---MAKEFILE CONTENTS---"
	@cd subdir && cat makefile
	@echo "---END MAKEFILE CONTENTS---"
	cd subdir && $(MAKE)

# Note that variables and exports. They are set/affected globally.
cooly = "The subdirectory can see me!"
export cooly
# This would nullify the line above: unexport cooly

clean:
	rm -rf subdir

你需要导出变量,使它们也能在 shell 中运行。

one=this will only work locally
export two=we can run subcommands with this

all: 
	@echo $(one)
	@echo $$one
	@echo $(two)
	@echo $$two

.EXPORT_ALL_VARIABLES 会为你导出所有变量。

.EXPORT_ALL_VARIABLES:
new_contents = "hello:\n\techo \$$(cooly)"

cooly = "The subdirectory can see me!"
# This would nullify the line above: unexport cooly

all:
	mkdir -p subdir
	printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
	@echo "---MAKEFILE CONTENTS---"
	@cd subdir && cat makefile
	@echo "---END MAKEFILE CONTENTS---"
	cd subdir && $(MAKE)

clean:
	rm -rf subdir

5.8 make的参数

make 有一系列可以运行的选项。查看--dry-run、--touch、--old-file。

你可以为 make 设置多个目标,例如,make clean run test 运行 clean 目标,然后运行,最后测试。
变量 Pt.
变量的种类和修改

变量有两种类型

递归(使用 =)--只在使用命令时查找变量,而不是在定义变量时。
简单扩展(使用 :=)--就像普通的命令式编程--只有那些目前已定义的变量才会被扩展

简单扩展(使用 :=)允许你追加变量。递归定义将导致无限循环错误。

只有在变量尚未被设置的情况下,......= 才会设置变量

行尾的空格不会被删除,但行首的空格会被删除。要使用单空格创建变量,请使用 $(nullstring)

未定义的变量实际上是一个空字符串!

使用 += 添加

字符串替换也是修改变量的一种常用且有用的方法。还可以查看文本函数和文件名函数。
命令行参数和覆盖

使用 override 可以覆盖命令行变量。在这里,我们使用 make option_one=hi 运行 make

命令列表和 define

define 指令并不是函数,尽管它看起来像函数。我见过它的使用频率很低,所以就不细说了,但它主要用于定义罐装配方,也可以与 eval 函数很好地搭配使用。

define/endef 简单地创建一个变量,并将其设置为一系列命令。请注意,这与在命令之间使用分号有点不同,因为每个命令都会在单独的 shell 中运行。

特定目标变量

可以为特定目标设置变量

特定模式变量

可以为特定目标模式设置变量

Makefile 的条件部分
条件 if/else

检查变量是否为空

检查变量是否已定义

ifdef 不会扩展变量引用;它只是查看是否定义了某个变量

$(MAKEFLAGS)

本例向您展示了如何使用 findstring 和 MAKEFLAGS 测试 make 标志。使用 make -i 运行此示例,可以看到它打印出 echo 语句。

函数
第一个函数

函数主要用于文本处理。使用 $(fn, arguments) 或 ${fn, arguments} 调用函数。Make 有大量内置函数。

如果要替换空格或逗号,请使用变量

不要在第一个参数之后的参数中包含空格。这将被视为字符串的一部分。文章来源地址https://www.toymoban.com/news/detail-747241.html

到了这里,关于Makefile教程1 快速入门的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 为什么阿里人能够快速成长?看完他们 Java 架构进化笔记,我秒懂!

    0-1 年入门: Java 基础复盘 (面向对象+Java 的超类+Java 的反射机制+异常处理+集合+泛型+基础 IO 操作+多线程+网络编程+JDK 新特性) Web 编程初探 (Servlet+MySQL 数据库+商品管理系统实战) SSM 从入门到精通 (Spring+SpringMVC+Mybatis+商品管理系统实战-SSM 版) SpringBoot 快速上手 (Spr

    2023年04月19日
    浏览(56)
  • 为什么黑客不黑/攻击赌博网站?如何入门黑客?

    攻击了,只是你不知道而已! 同样,对方也不会通知你,告诉你他黑了赌博网站。 攻击赌博网站的不一定是正义的黑客,也可能是因赌博输钱而误入歧途的法外狂徒。之前看过一个警方破获的真实案件:28岁小伙因赌博无法提款自学成为黑客,攻击境外博彩网站日进万元,最

    2023年04月11日
    浏览(68)
  • 【Golang】三分钟让你快速了解Go语言&为什么我们需要Go语言?

    博主简介: 努力学习的大一在校计算机专业学生,热爱学习和创作。目前在学习和分享:数据结构、Go,Java等相关知识。 博主主页: @是瑶瑶子啦 所属专栏: Go语言核心编程 近期目标: 写好专栏的每一篇文章 Go 语言从 2009 年 9 月 21 日开始作为谷歌公司 20% 兼职项目,即相关

    2023年04月21日
    浏览(59)
  • 入门ElasticSearch :为什么选择ES作为搜索引擎?

    随着数据量的不断增长,搜索和分析大规模数据集变得越来越重要。传统数据库在面对这种需求时往往表现不佳,这时候就需要一种专门用于搜索和分析的引擎。ElasticSearch (简称ES)就是这样一款强大的搜索引擎,它具有许多优势,使得它成为许多企业和开发者的首选。 简

    2024年02月09日
    浏览(46)
  • 为什么无法加入家庭组 加入家庭组图文方法教程

    为什么无法加入家庭组,是什么原因导致无法加家庭组呢,看完西西的教程想必大家应该怎么解决了把。 解决此问题 单击“运行”,然后按照向导中的步骤操作。 如果运行 Fix It 向导无法解决问题,请参考下面的列表: 您的网络中没有可用的家庭组。若要创建一个家庭组,

    2024年02月06日
    浏览(54)
  • Notion搭建个人博客教程, 以及为什么选用notion

    个人博客链接:williamhjr.top WILLIAMHJR的博客 | 一个专注于个人提升,技术沉淀的博客 Author:William Huang 目的 : ①能够有助于沉淀技术, 学过的知识用自己的话总结输出,费曼学习法 ②记录个人的想法与思考, 构建个人知识库 ③方便分享博文给别人, 提升沟通效率 ④储备足够多的知识

    2024年02月09日
    浏览(47)
  • 【C++入门】你知道C++为什么会有内联函数吗?

    👦个人主页:@Weraphael ✍🏻作者简介:目前学习C++和算法 ✈️专栏:C++航路 🐋 希望大家多多支持,咱一起进步!😁 如果文章对你有帮助的话 欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨ 本章是补充C语言语法的不足,以及C++是如何对C语言设计不合理的地方进行优化的。 我

    2024年02月03日
    浏览(71)
  • 【Golang】一篇文章带你快速了解Go语言&为什么你要学习Go语言

    目录 1. 为什么互联网世界需要Go语言 1.1 硬件限制:摩尔定律已然失效  1.2 Go语言为并发而生 1.3 Go性能强悍 1.4 Go语言简单易学 1.4.1 语法简洁 1.4.2 代码风格统一 1.4.3开发效率高  2.Go语言的诞生与发展 2.1什么是Go语言   2.2 Go语言的诞生 2.3 Go Gopher——Go语言的吉祥物 3. 为什么

    2024年02月04日
    浏览(61)
  • Linux教程——为什么要学Linux,它比Windows好在哪里?

    学习Linux对于计算机科学和技术领域的学生和专业人士来说,是一个非常重要的事情。Linux是一种自由和开源的操作系统,它具有许多优点和特点,使其成为一个值得学习的选择。 在本文中,将介绍一下为什么学习Linux以及它与Windows相比的优势。 Linux是一个开源操作系统,这

    2024年02月07日
    浏览(207)
  • 为什么要桥接无线路由器 无线路由器桥接图文教程

    为什么要桥接无线路由器?无线桥接的好处,桥接可以把两个不同物理位置的,不方便布线的用户连接到同一局域网。 无线桥接还可以起到信号放大的作用。我们可以把多个无线路由器桥接到一起,以实现信号的覆盖范围。至少有一台路由器支持WDS功能,但两台路由器的牌子

    2024年02月06日
    浏览(49)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包