Make & Makefile

Makefile 文件

makefile 是个文件,这个文件中描述了程序的编译规则.

采用 Makefile 的好处

  1. 简化编译程序的时候输入得命令,编译的时候只需要敲 make 命令就可以了
  2. 可以节省编译时间,提高编译效率

make命令

是什么

代码变成可执行文件,叫做编译(compile);先编译这个,还是先编译那个(即编译的安排),叫做构建(build)。

make 作为最常用的构建工具,是个 命令 即 可执行程序,用来解析 Makefile 文件的命令

  • 规定要构建哪个文件、
  • 它依赖哪些源文件,
  • 当那些文件有变动时,如何重新构建它。

命令格式

# 一般使用的时候直接在makefile文件的目录下 make就可以
# 默认的找名为GNUmakefile、makefile、Makefile的文件
# 一般在makefile文件目录下,就直接使用make命令
make

# 完整命令
make [ ‐f file ] [ targets ] [变量=值]
输入文件
  • [ -f file ]: 指定给定名字以外的文件作为makefile输入文件

    • 一般是不填的,在makefile文件目录下,直接使用make命令

      默认找 GNUmakefilemakefileMakefile作为 makefile输入文件

    • ,也可以指定以上名字以外的文件作为makefile输入文件

    • 使用-f 指定

目标文件
  • [ targets ]: 指定了make工具要实现的目标
    • 没有指定目标时,
      • 默认会实现makefile文件内的第一个目标
      • 然后退出,
    • 也可以指定了make工具要实现的目标,
    • 目标可以是一个或多个(多个目标间用空格隔开)
传参
  • 传参

    • make 工具传给 makefile 的变量 执行 make 命令时,
    • make 的参数 options 也可以给 makefile 设置变量。
    make cc=arm-linux-gcc
    # 传参cc=arm-linux-gcc,交叉编译工具
    

示例

# 目标: 	  main		# ./main  执行
# 依赖文件:  main.o sub.o sum.o 
main:main.o sub.o sum.o 
    gcc main.o sub.o sum.o ‐o main 
main.o:main.c 
    gcc ‐c main.c ‐o main.o 
sub.o:sub.c 
    gcc ‐c sub.c ‐o sub.o 
# 假想目标: clean # make clean # 发动清理
clean:
    rm *.o main a.out ‐rf # 清除中间文件

makefile 结构

使用TAB而不是空格

目标:依赖文件列表 		# 完成链接
	命令列表(第一行)	 # 预处理、编译、汇编,生成二进制文件
	命令列表(第二行)
	...
clean:  # 假想目标
    rm main *.o

简单的例子:

# 目标: 	  main		# ./main  执行
# 依赖文件:  main.c main.h 
main:main.c main.h
	# 依赖文件1: main.c
	# 依赖文件2: main.h
	# 目标文件:  main
    gcc main.c main.h -o main
# 假想目标: clean
# make clean 发动清理
clean:
    rm main *.o  # 清除中间文件

规则

Makefile文件由一系列规则(rules)构成。每条规则就明确两件事:构建目标的前置条件是什么,以及如何构建,形式如下:

<target> : <prerequisites> 
[tab]  <commands> 
  • 完成链接 <target> : <prerequisites>

    $(target):$(prerequisites)

    • 目标(target)必选:

      通常是要产生的文件名称,目标可以是可执行文件或其它 obj 文件,也可是一个动作的名称

    • 依赖文件/前置条件(prerequisites):

      用来输入从而产生目标的文件一个目标通常有几个依赖文件(可以没有)

  • 生成二进制文件

    所有的 .c到.o文件的转化 (完成 预处理、编译、汇编的工作)

    .o:%*.c

    • tab键起首([tab])

    • 命令(commands):

      • make 执行的动作,一个规则可以含几个命令(可以没有)

      • 有多个命令时,每个命令占一行

命令合集

命令包 define

命令包有点像是个函数, 将连续的相同的命令合成一条, 减少 Makefile 中的代码量, 便于以后维护。

语法:

define <command-name>
command
...
endef
# Makefile 内容
define run_demo_makefile
    @echo -n "Hello"
    @echo " Makefile!"
    @echo "这里可以执行多条 Shell 命令!"
    echo "参数1 $(1)" >> $(2)
endef

all:
	$(call run_demo_makefile)
define generate-common-build-props-with-product-vars-set
	BUILD_FINGERPRINT="$(BUILD_FINGERPRINT_FROM_FILE)" \
	BUILD_ID="$(BUILD_ID)" \
	BUILD_NUMBER="$(BUILD_NUMBER_FROM_FILE)" \
	BUILD_VERSION_TAGS="$(BUILD_VERSION_TAGS)" \
	DATE="$(DATE_FROM_FILE)" \
	PLATFORM_SDK_VERSION="$(PLATFORM_SDK_VERSION)" \
	PLATFORM_VERSION="$(PLATFORM_VERSION)" \
	TARGET_BUILD_TYPE="$(TARGET_BUILD_VARIANT)" \
	bash $(BUILDINFO_COMMON_SH) "$(1)" >> $(2)
endef
$ make
Hello Makefile!
这里可以执行多条 Shell 命令!
假想目标

默认执行第一个假想目标。

假想目标(可选)

隐式声明

伪目标可以这样来理解,伪目标的存在可以帮助我们找到命令并执行。

  • 它并不会创建目标文件,假想目标并不是一个真正的文件名,

  • 通常是一个目标集合或者动作(可以没有依赖或者命令)

    只是想去执行这个目标下面的命令。

clean:  # clean 就是假想目标
    rm *.o main a.out ‐rf

一般需要显示的使用make + 名字 显示调用

make clean // 发动清理

当工作目录下不存在以 clean 命令的文件时,在 shell 中输入 make clean 命令,

命令 rm *.o main a.out ‐rf 会被执行(执行假想对象clean的命令)

显式声明 .PHONY

而且当一个目标被声明为伪目标之后,make 在执行此规则时不会去试图去查找隐含的关系去创建它。

  • 提高了 make 的执行效率,

  • 不用担心目标和文件名重名而使编译失败。

.PHONY:clean
clean:
    rm -rf *.o test

.PHONY后面跟的目标都被称为伪目标,也就是说 make 命令后面跟的参数,

  • 如果 make 命令后的参数出现在伪目标中
    • 直接在Makefile中就执行伪目标的依赖和命令。
    • 不管Makefile同级目录下是否有该伪目标同名的文件,即使有也不会产生冲突。
  • 可以提高执行makefile时的效率。
同名文件

均存在的情况下,优先执行 伪目标。否则有啥执行啥。

比如 make objectX 同级目录下

  • 有同名文件 objectXMakefile

    • 没有伪目标。执行objectX文件中的内容

    • 有伪目标。执行伪目标中的内容

      .PHONY: objectX
      objectX: $(modules_to_install) \
             $(INSTALLED_ANDROID_INFO_TXT_TARGET)
      
  • 没有同名文件objectX

    执行Makefile中的伪目标

符号和变量

常见符号

$ @ * % -

注释

井号(#)在 Makefile 中表示注释

# 这是注释
result.txt: source.txt
    # 这是注释
    cp source.txt result.txt # 这也是注释
执行命令 @

@通常用在“规则”行,在命令的前面加上@。

表示不显示命令本身只显示它的结果,可以关闭回声。

回声:@echo

正常情况下,make会打印每条命令,然后再执行,这就叫做回声(echoing)

test:
    @echo TODO
    
CMD_MKOBJDIR=if [ -d ${DIR_OBJ} ]; then exit 0; else mkdir ${DIR_OBJ}; fi
@${CMD_MKOBJDIR}
通配符 * ? []

通配符(wildcard)用来指定一组符合条件的文件名。Makefile 的通配符与 Bash 一致,主要有星号(*)、问号(?)和 []:

通配符含义
*0个或者是任意个字符
?任意一个字符
[]指定匹配的字符放在 “[]” 中

比较常用的就是 * 号

.PHONY:clean
clean:
        rm -f *.o
模式匹配 %

Make 命令允许对文件名,进行类似正则运算的匹配,主要用到的匹配符是 %。比如,假定当前目录下有 f1.c 和 f2.c 两个源码文件,需要将它们编译为对应的对象文件。

%.o: %.c

等同于下面的写法。

f1.o: f1.c
f2.o: f2.c

使用匹配符 %,可以将大量同类型的文件,只用一条规则就完成构建。

忽略错误-

忽略掉错误,继续执行,

通常删除,创建文件如果碰到文件不存在或者已经创建,会选择忽略并继续执行

-rm dir
-mkdir aaadir

使用 -include

  • 忽略由于包含文件不存在或者无法创建时的错误提示

  • - 的意思是告诉make,忽略此操作的错误。make继续执行

使用 include

  • 不加-,当文件出错或者不存在的时候, make 会报错并退出。
-include $(TARGET_DEVICE_DIR)/AndroidBoard.mk

inherit-product 函数:表示继承另外一个文件

  • 使用 inherit-product包含其它文件。同样的变量会被追加而不是覆盖。
  • 并确保不会两次包含同一个 makefile 。

例如:

在 A.mk 中 PRODUCT_VAR := a,在 B.mk 中PRODUCT_VAR := b 。在 A.mk 中:

  • include B.mk,得到 PRODUCT_VAR := b
  • inherit-product B.mk,得到 PRODUCT_VAR := a b

变量

概念

makefile 变量类似于 C 语言中的宏,当 makefile 被 make 工具解析时,其中的变量会被展开。

变量的作用:

​ 保存 - 文件名列表 文件目录列表 编译器名 编译参数编译的输出

分类:

  • 自定义变量
  • 系统环境变量 setenv设置的
  • 预定义变量(自动变量) $@ $^ $< $? $%
变量引用 $ $$

$ :扩展打开makefile中定义的变量

$ VAR
$(VAR)
${VAR}

$$:扩展打开makefile中定义的shell变量

$$ VAR
$$(VAR)
$${VAR}
引号 "" ''

若变量中本身就包含了空格,则整个字符串都要用双引号或单引号括起来。

双引号内的特殊字符可以保有变量特性,但是单引号内的特殊字符则仅为一般字符

# 输出的可能是 str = abcde...等等
echo "str = $str"
#输出一定是 str = $str
echo 'str = $str'   
变量赋值 =
= 递归展开

= 赋值,赋予整个makefile中最后被指定的值。

使用 = 来定义的变量是递归展开的 (Recursively Expanded),直到该变量被使用时等号右边的内容才会被展开。而且每次使用该变量时,等号右边的内容都会被重新展开。

  • 好处:可以把变量的真实值推到后面来定义。
  • 缺点:递归定义可能导致出现无限循环展开,尽管 make 能检测出这样的无限循环展开并报错。
VIR_A = A
VIR_B = $(VIR_A) # B的值是之后的AA
VIR_A = AA
:= 简单展开

直接赋值,赋予当前位置的值

简单展开 (Simply Expanded)。读到变量定义这一行时 等号右边立即被展开,引用的所有变量也会被立即展开。前面的变量不能使用后面的变量,只能使用前面已定义好了的变量。

VIR_A := A
VIR_B := $(VIR_A) # B的值是此时的A 
VIR_A := AA

使用这种方法可以在变量中引入开头空格。见下面的示例:

nullstring :=
space := $(nullstring) # end of the line

nullstring 是一个 Empty 变量,其中什么也没有,而 space 的值是一个空格。因为在操作符的右边是很难描述一个空格的,这里采用的技术很管用。先用一个 Empty 变量来标明变量的值开始了,而后面采用 # 注释符来表示变量定义的终止,这样,我们可以定义出其值是一个空格的变量。

?= 条件变量赋值

如果该变量没有被赋值,则赋予等号后的值。如果先前被定义过,那么将什么也不做。

?= 是递归展开的。会使用最后的值。见上文 = 递归展开

VIR ?= old_value # VIR在之前没有被赋值, 设置为old_value
VIR ?= new_value # VIR在之前被赋值, 保留old_value
+= 追加变量值
  • 字符串拼接
  • 将等号后面的值添加到前面的变量上
自定义变量

在 makefile 文件中定义的变量。 make 工具传给 makefile 的变量。

一般都在 makefile 的头部定义

可以以数字开头,大小写敏感

定义 清除
# 自定义变量语法 
变量名=变量值  # 不可以有空格

# 清除变量
## 使用 unset 命令清除变量
unset varname

CC=gcc
target=main
obj1=sub
obj2=sum
OBJ=main.o sub.o sum.o
显示 只读 读入
# 显示变量
## 使用 echo 命令可以显示单个变量取值
echo $num
echo "num = $num"

# 只读变量
## 使用readonly创建只读变量
readonly n=999

# 读取数据
## 使用read从终端读取数据保存在变量中 
read str
# 可以通过 `$1  $2... ${10}...`获取函数参数 
系统环境变量(与shell联系)

make 工具解析 makefile 前,读取系统环境变量并设置为 makefile 的变量。

在此之前,shell已经介绍了系统环境变量。

见:shell - 变量 - 变量分类 - 系统环境变量

传统上,所有环境变量均为大写.

预定义变量

makefile中有许多预定义变量,这些变量具有特殊的含义,可在makefile中直接使用。

$@ $^ $< $? $%

Make 命令还提供一些自动变量,它们的值与当前规则有关。

自动变量含义
$@目标文件(target)——就是 Make 命令当前构建的那个目标。比如,make foo$@ 就指代 foo。如果目标不是函数库文件(Unix下是[.a],Windows下是[.lib]),那么其值为空。
$^ 所有的依赖文件(components)
$< 第一个依赖文件(components中最左边的那个)
$?比目标还要新的依赖文件列表。以空格分隔。
$%仅当目标是函数库文件中,表示规则中的目标成员名。
$*指代匹配符 % 匹配的部分, 比如% 匹配 f1.txt 中的f1 ,$* 就表示 f1

例如,$@

  • 如果一个目标是"foo.a(bar.o)“,那么,” %"就是"bar.o"," @“就是"foo.a”。
  • 如果目标不是函数库文件(Unix下是[.a],Windows下是[.lib]),那么,其值为空。
# * 0 ? $ 入参 函数
预设变量含义
$#传给shell脚本参数的数量
$0当前执行的进程名
$*位置变量$0 - $9 保存从终端输入的每一个参数
传给shell脚本参数的内容$1、$2、$3
${10}编号大于9使用{},运行脚本时传递给其的参数,用空格隔开
$?命令执行后返回的状态
$?获取函数返回值用于检查上一个命令执行是否正确.
(在Linux中,命令退出状态为0表示该命令正确执行,任何非0值表示命令出错)。但是$?获取到返回值如果超过255,会出错
$$当前进程的进程号
最常见的用途是用作临时文件的名字以保证临时文件不会重复
@ < ^ 编译有关
预设变量含义
$@目标名,目标文件
$<依赖文件列表中的第一个文件
$^依赖文件列表中除去重复文件的部分(所有的依赖文件)
CCC编译器的名称,默认值为cc
CFLAGSC编译器的选项
CPPC预编译器的名称,默认值为$(CC) -E
CPPFLAGSC预编译的选项
CXXC++编译器的名称,默认值为g++
CXXFLAGSC++编译器的选项
AR归档维护程序的程序名,默认值为ar
ARFLAGS归档维护程序的选项
AS汇编程序的名称,默认值为as
ASFLAGS汇编程序的选项

示例

精简版:
CC=gcc
obj=main
obj1=sub
obj2=sum
OBJ=main.o sub.o sum.o
CFLAGS=-Wall -g
$(obj):$(OBJ)
    $(CC) $^ -o $@

$(obj).o:$(obj).c
    $(CC) $(CFLAGS) -c $< -o $@
$(obj1).o:$(obj1).c
    $(CC) $(CFLAGS) -c $< -o $@
$(obj2).o:$(obj2).c
    $(CC) $(CFLAGS) -c $< -o $@

clean:
    rm *.o $(obj) a.out -rf

​ 最精简版

CC=gcc
obj=main
obj1=sub
obj2=sum
OBJ=main.o sub.o sum.o
CFLAGS=-Wall -g
# 完成链接的工作。
$(obj):$(OBJ)
    $(CC) $^ -o $@
# 所有的 .c到.o文件的转化 (完成 预处理、编译、汇编的工作)
%*.o:%*.c
    $(CC) $(CFLAGS) -c $< -o $@

clean:
    rm *.o $(obj) a.out -rf

.o:%*.c 所有的 .c到.o文件的转化 (完成 预处理、编译、汇编的工作)

$(obj):$(OBJ) 完成链接的工作。

gcc -E hello.c -o hello.i 1、预处理

gcc -S hello.i -o hello.s 2、编译

gcc -c hello.s -o hello.o 3、汇编

gcc hello.o –o hello 4、链接

gcc *.c 编译所有.c文件

gcc *.o 编译所有.o文件

判断和循环

Makefile 条件判断作用

条件语句可以根据一个变量的值来控制 make 执行或者时忽略 Makefile 的特定部分,条件语句可以是两个不同的变量或者是常量和变量之间的比较。

Makefile 使用 Bash 语法,完成判断和循环。

条件语句

下面是条件判断中使用到的一些关键字:

关键字 功能

ifeq 判断参数是否不相等,相等为 true,不相等为 false

ifneq 判断参数是否不相等,不相等为 true,相等为 false

ifdef 判断是否有值,有值为 true,没有值为 false

ifndef 判断是否有值,没有值为 true,有值为 false

ifeq ifneq

判断两个参数是否 相等/不相等

使用方式如下

ifeq (first, second)
ifeq 'first' 'second'
ifeq `first` `second`
ifeq `first` 'second'
ifeq 'first' `second`

#判断当前编译器是否 gcc ,然后指定不同的库文件
ifeq ($(CC),gcc)
  libs=$(libs_for_gcc)
else
  libs=$(normal_libs)
endif

三个关键字 ifeq、else、endif。其中:

  • ifeq 表示条件语句的开始,并指定一个比较条件(相等)。括号和关键字之间要使用空格分隔,两个参数之间要使用逗号分隔。参数中的变量引用在进行变量值比较的时候被展开。ifeq后面的是条件满足的时候执行的,条件不满足忽略;

  • else 表示当条件不满足的时候执行的部分,不是所有的条件语句都要执行此部分;

  • endif 是判断语句结束标志,Makefile 中条件判断的结束都要有;

    其实 ifneq 和 ifeq 的使用方法是完全相同的,只不过是满足条件后执行的语句正好相反

使用

first = $(CXX)
second = g++

all:
ifeq ($(first), $(second))
	echo `first == second`
else
	echo `first != second`
endif

输出

$ make  

first == second
ifdef ifndef

主要功能是判断变量是否 已定义(不为空)

当我们需要判断一个变量的值是否为空的时候需要使用ifeq 而不是 ifdef

ifdef VARIABLE_NAME
a =
b = $(a)

TestIfdefA:
ifdef a
	echo yes
else
	echo  no
endif

TestIfdefB:
ifdef b
	echo yes
else
	echo  no
endif
$ make TestIfdefA
no

$ make TestIfdefB
yes

循环

# 循环
LIST = one two three
all:
    for i in $(LIST); do \
        echo $$i; \
    done

# 等同于
all:
    for i in one two three; do \
        echo $$i; \
    done  

常用函数

wildcard 通配符函数
$(wildcard _pattern)

wildcard函数,通配符函数,得到当前工作目录中满足_pattern模式的文件或目录名列表

SRCS = $(wildcard *.c)

all:
        echo $(SRCS)

测试结果如下:

# ls
abc.h  a.c  b.c  c.c  Makefile
# make
echo a.c b.c c.c
a.c b.c c.c
路径
abspath
$(abspath _names)

将_names中的各路径转换成绝对路径,并将转换后的结果返回。

ROOT := $(abspath /usr/../lib)

all:
        echo $(ROOT)   
realpath
$(realpath _names)

用于获取_names所对应的真实路径,测试代码如下

ROOT := $(realpath ./..)

all:
        @echo $(ROOT)
前缀后缀
addprefix 前缀
$(addprefix _prefix, _names)

给名字列表_names中的每一个名字增加前缀_prefix,将增加了前缀的名字列表返回。

without_dir = main.c bar.c foo.c
with_dir := $(addprefix src/, $(without_dir))

all:
        echo $(with_dir)
addsuffix 后缀
$(addsuffix _suffix, _names)

给名字列表_names中的每一个名字增加后缀_suffix,将增加了后缀_suffix的名字列表返回。

without_suffix = main foo bar
with_suffix := $(addsuffix .c, $(without_suffix))
all:
        echo $(with_suffix)
eval 执行
$(eval _text)

使make将再一次解析_text语句。

例如:执行过滤,并将过滤后的结果赋给 source变量

sources = foo.c bar.c baz.s ugh.h
$(eval sources := $(filter %.c %.s, $(sources)))

all:
        echo $(sources)
filter
filter 返回满足

filter: 过滤语句,

$(filter _pattern, _text)

从一个名字列表_text中根据模式_pattern得到满足需要的名字列表返回。

  • 过滤掉不符合指定的模式的内容,仅保留符合指定的模式的内容。

  • 在过滤时,大小写敏感(区分大小写)

SOURCE := 1 2 3 4 5
# 指定的模式为 1 2 3,多个模式之间用空格区分
$(filter 1 2 3 , $(SOURCE))
# 上式返回值为
# a b c

例如:返回后缀为 .c .s的文件

sources = foo.c bar.c baz.s ugh.h
sources := $(filter %.c %.s, $(sources))

all:
        echo $(sources)

ifneq + filter 场景:

  • 某项目多个版本(A,B, C),同时进行开发。

  • 除了代码中的一些宏开关外,在编译时,也需要进行不同版本的判断。

    • 版本A、B,编译某模块时需要一个特殊参数。

    • 版本C,编译该模块时,不需要该特殊参数。

# 如果 TARGET 为A 或 B(即不为空),那么加入某些特殊参数
# 这里ifneq第二个参数为NULL
ifneq ($(filter A B , $(TARGET)),)
   # 版本A、B才需要的某些特殊参数
endif
filter-out 不满足
$(filter-out _pattern, _text)

从名字列表_text中根据模式_pattern滤除一部分名字,将滤除后的列表返回。

  • 返回不满足
objects = main1.o foo.o main2.o bar.o 
result = $(filter-out main%.o, $(objects)) 

all: 
        @echo $(result) 
notdir 获取文件名
$(notdir _names)

从路径_names中抽取文件名,并将文件名返回。(去除路径,获取文件名)

file_name := $(notdir code/foo/src/foo.c code/bar/src/bar.c)

all:
        @echo $(file_name)
替换
patsubst
$(patsubst _pattern, _replacement, _text)

将名字列表_text中符合_pattern模式的名字替换为_replacement,将替换后的名字列表返回。

该函数可以用于替换变量后缀

例如:c文件后缀名全部替换成了.o格式

SRCS = $(wildcard *.c)
OBJS = $(patsubst %.c, %.o, $(SRCS))

all:
        echo $(SRCS)
        echo $(OBJS)

测试结果如下所示:

# make
echo a.c b.c c.c
a.c b.c c.c
echo  a.o  b.o  c.o
a.o b.o c.o
=

可以采用变量赋值的高级用法,即在赋值的时候,完成文件名后缀替换工作

mixed = foo.c bar.c main.o

objects = $(mixed:.c=.o)

all:
        @echo $(objects)

以上两种替换方法都是可行的

strip 清除空格

strip 去空字符语句,去掉字串中开头和结尾的空字符(空字符包括空格、[Tab]等不可显示字符)。

VAR=    1 2 3  
$(strip $(VAR))
# 结果是:
# 1 2 3

代码控制

宏 CFLAGS

控制源码:在Makefile 中添加宏定义可以用来控制源码的编译,

通过CFLAGS中的选项-D定义

CFLAGS += -DMY_DEBUG
#ifdef MY_DEBUG
    printf("debug on...\n");
#endif

-include

控制 Makefile 中 的相互导入

使用 -include

  • 忽略由于包含文件不存在或者无法创建时的错误提示

  • - 的意思是告诉make,忽略此操作的错误。make继续执行

使用 include

  • 不加-,当文件出错或者不存在的时候, make 会报错并退出。
-include $(TARGET_DEVICE_DIR)/AndroidBoard.mk

inherit-product 函数:表示继承另外一个文件

  • 使用 inherit-product包含其它文件。同样的变量会被追加而不是覆盖。
  • 自动确保不会两次包含同一个 makefile 。
# # 例如
PRODUCT_VAR := a	# 在 A.mk 中
PRODUCT_VAR := b	# 在 B.mk 中

# 在 A.mk 中
inherit-product B.mk
# # 得到 PRODUCT_VAR := a b

include B.mk 或 -include B.mk
# # 得到 PRODUCT_VAR := b
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值