当前位置:   article > 正文

使用Makefile笔记总结_makefile print

makefile print

一、简单了解Makefile

1.1 Makefile示例

使用Makefile编写规则编写一个输出Hello world的程序,程序文件如下:

$ cat print.h 
#include<stdio.h>
void printhello();

$ cat print.c
#include"print.h"
void printhello(){
	printf("Hello, world\n");
}

$ cat main.c 
#include "print.h"
int main(void){
	printhello();
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

编写的Makefile文件如下:

helloworld : main.o print.o
	cc -o helloworld main.o print.o
mian.o : mian.c print.h
	cc -c main.c
print.o : print.c print.h
	cc -c print.c

clean :
	rm helloworld main.o print.o
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

1.2 基本规则

make命令执行时,需要一个 Makefile 文件,以告诉make命令需要怎么样的去编译和链接程序。以下是Makefile最基本的规则:

target ... : prerequisites ...
	command
	...
	...
  • 1
  • 2
  • 3
  • 4
  1. target就是一个目标,可以是Object File,也可以是执行文件,还可以是一个标签(Label)。
  2. prerequisites就是要生成那个target所需要的文件或是target。如果是target,则该target在后面会有一个对应的规则(eg:print.o)。
  3. command就是make需要执行的命令。command前必须使用[Tab]键,使用空格会报错。

make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期要比targets文件的日期要新,或者target不存在的话,那么,make就会执行后续定义的命令。

1.3 make是如何工作的

下文所说的helloworld目标,指的是Makefile中的target,helloworld文件 指的是项目中名为helloworld的文件,注意区分。在默认的方式下,也就是我们只输入make命令,那么:

  1. make会在当前目录下找名字叫 Makefile 或 makefile 的文件。
  2. 如果找到,它会找文件中的第一个目标target。如上例中,会找到helloworld目标,并将其作为最终目标文件的文件名。然后从helloworld目标开始依次寻找依赖关系。
  3. 如果helloworld文件存在(条件1),且helloworld目标文件比其所依赖的.o目标文件的文件修改时间新(条件2),且.o目标文件比其所依赖的.c.h文件的文件修改时间新(条件3),则make啥也不做
  4. 条件1不成立,条件2和3成立,则只会执行helloworld目标定义的命令。
  5. 条件2不成立,条件3成立,则不管条件1是否成立,只会执行helloworld目标定义的命令。
  6. 如果条件3不成立,不管条件1和2是否成立,helloworld目标以及.o目标中定义的命令,都会执行。.o目标有两个,哪个不满足条件就执行哪个目标定义的命令,满足的那个不执行。

这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。

1.4 使用变量

在Makefile示例中,main.oprint.o在prerequisites和command总共出现了3次,如果再有新的xxx.o规则,在这三个地方都要加。所以,为了makefile的易维护,在makefile中我们可以使用变量。如下就是对变量OBJECTS的定义和使用:

OBJECTS = main.o print.o

helloworld: $(OBJECTS)
	cc -o helloworld $(OBJECTS)
mian.o: mian.c print.h
	cc -c main.c
print.o: print.c print.h
	cc -c print.c

clean:
	rm helloworld $(OBJECTS)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

1.5 make自动推导

GNU的make很强大,它具有一些隐晦规则,可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个.o文件后都写上类似的命令,因为,我们的make会自动识别,并自己推导命令。

只要make看到一个.o文件,它就会自动的把.c.h文件加在依赖关系中,如make找到一个print.o,那么print.cprint.h,就会是print.o的依赖文件,并且 cc -c print.c 也会被推导出来。

OBJECTS = main.o print.o

helloworld: $(OBJECTS)
	cc -o helloworld $(OBJECTS)
mian.o:
print.o:

clean:
	rm helloworld $(OBJECTS)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

甚至还可以按照下面方式写,不过这样Makefile文件依赖关系就显得有点凌乱了,可以根据实际情况选择,别人写了要能看懂。

OBJECTS = main.o print.o

helloworld: $(OBJECTS)
	cc -o helloworld $(OBJECTS)
$(OBJECTS):

clean:
	rm helloworld $(OBJECTS)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

二、变量

在 Makefile 中的定义的变量,就像是 C/C++语言中的宏一样,他代表了一个文本字符串,在 Makefile 中执行的时候其会自动原模原样地展开在所使用的地方。其与 C/C++所不同的是,你可以在 Makefile 中改变其值。

变量的命名字可以包含字符、数字,下划线(可以是数字开头),但不应该含有:#=或是空字符(空格、回车等)。变量是大小写敏感的, fooFooFOO是三个不同的变量名。

2.1 变量的定义和引用

定义或改变一个变量常用以下4种等号:

  • :=:立即变量。对于右边引用的变量,在定义左边变量时就会展开,且只有到目前为止定义过的变量才会得到展开。
  • =:延迟变量。对于右边引用的变量,在执行含有左边变量的命令时才会展开,而不是在定义左边变量时展开。
  • ?=:如果左边变量之前没有被定义过,那么变量的值就是右边值,如果变量先前被定义过,那么这条语将什么也不做。
  • +=:将等号右边的值追加到左边变量中。如果变量之前没有定义过,那么,+=会自动变成=;如果前面有变量定义,那么+=会继承于前次操作的赋值符;如果前一次的是:=,那么+=会以:=作为其赋值符。

变量可以使用在许多地方,如规则中的“目标”、“依赖”、“命令”以及“变量的值”中。引用一个变量可使用$()${}这两个符号,使用第一个居多。

one = hello
#不允许将变量自己的值赋给自己,因为one会递归引用自身
#one = ${one} world
#这样写就允许,不会递归引用
one := ${one} world #这里加注释后,one的值为"hello world ",后面多一个空格

all:
	echo $(one)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

2.2 变量的两种高级用法

第一种是变量值的替换,其格式为$(var:a=b)或是${var:a=b},意思是,把变量var中所有以a子串结尾a替换成b子串。这里的结尾意思是空格或是结束符

OBJECTS = main.o print.o
SOURCE = $(OBJECTS:.o=.c)
all:
	echo $(SOURCE)
  • 1
  • 2
  • 3
  • 4

第二种是把变量的值再当成变量,其格式为$($(var)),意思是,把变量var的值作为变量名并对其引用。

x = y
y = z
z = value
a := $($($(x)))
  • 1
  • 2
  • 3
  • 4

2.3 override 和 define 关键字

make是可以通过命令行设置变量和值的,使用override定义一个变量,则通过命令行对这个变量的赋值会被忽略。

# 执行make one=boy会输出boy
one = hello
all:
        echo $(one)

# 执行make one=boy会输出hello,override会忽略命令行对one的赋值
override one = hello
all:
        echo $(one) 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

define后面跟的是变量的名字,而重起一行定义变量的值或执行命令,定义是以endef关键字结束。

define实际上只是一个命令列表,这与命令之间的分号有所不同,因为列表的每个命令都在单独的shell中运行。Linux命令行在shell脚本和Makefile会有些不一样的差别,一个shell是一个进程,shell脚本的命令都是在shell一个进程进行,前后命令会有所影响;而makefile里的每一行命令是一个单独的进程,只在单行里有影响,不对上下文影响。

one = export blah="I was set!"; echo $$blah

define two
export blah=set
echo $$blah
endef

all: 
	@echo "这会打印 'I was set'"
	@$(one)
	@echo "这不会打印 'I was set' 因为每个command都在单独的shell中运行"
	@$(two)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

2.4 环境变量与目标变量

除了用户自定义的一些变量,make在解析Makefile中还会引入一些系统环境变量,如编译参数CFLAGS、SHELL、MAKE等。这些变量在make开始运行时被载入到Makefile文件中,因为是全局性的系统环境变量,所以这些变量对所有的Makefile都有效。若Makefile中有用户自定义的同名变量,系统环境变量将会被用户自定义的变量覆盖。若用户在命令行中传递跟系统环境变量同名的变量,系统环境变量也会被传递的同名变量覆盖。(如果make指定了-e参数,那么,系统环境变量将覆盖Makefile中定义的变量)

one = hello
all:
	@echo $(one)

$ export one=boy
$ make
hello
$ make -e
boy
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

为特定的目标定义变量,该变量的作用域只在特定目标下,且在作用域内是被最先匹配的,即先于文件变量和环境变量(前提make执行时没加-e参数)。

one = boy
all: one = cool

all:	# 输出coll
	@echo $(one)

other:	# 输出boy
	@echo $(one)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

2.5 自动变量

自动变量是局部变量,作用域范围在当前的规则内,它们分别代表不同的含义:

  • -:告诉make在编译时忽略所有的错误
  • @:告诉make在执行命令前不要显示命令
  • $@:所有目标文件
  • $^:所有目标依赖
  • $<:目标依赖列表中的第一个依赖
  • $?:所有目标依赖中被修改过的文件
  • $%:当规则的目标是一个静态库文件时,$%代表静态库的一个成员名
  • $+:类似$^,但是保留了依赖文件中重复出现的文件
  • $*:在模式匹配和静态模式规则中,代表目标模式中%的部分。比如hello.c,当匹配模式为%.c时,$*表示hello
  • $(@D):表示目标文件的目录部分
  • $(@F):表示目标文件的文件名部分
  • $(*D):在模式匹配中,表示目标模式中%的目录部分
  • $(*F):在模式匹配中,表示目标模式中%的文件名部分

三、Makefile规则

3.1 通配符

在Makefile中,常用的通配符是*%。两者的共同点是都代表任意长度的字符;两者的区别在于,*是应用在当前目录中来匹配文件或目录,%是应用在当前文件中来匹配Makefile相应规则。两者的应用场合为:

  • *主要应用在规则的依赖中、规则的命令中、以及变量的值中。除了命令中,在其他地方都不建议直接使用通配符,而是用一些函数,如想列举当前目录下的所有C文件,可用$(wildcard *.c)
  • %主要应用在规则的目标中、规则的依赖中、以及一些函数中(字符串查找替换等)。

用在规则的目标和依赖中,make在读取Makefile时会自动对其进行匹配处理(通配符展开)。用在规则的命令中,通配符的通配处理在shell执行命令时完成。

# 找到当前目录下所有以.c为后缀的文件,将后缀.c替换成.o,如:main.c => main.o,然后作为依赖
all: $(subst .c,.o,$(wildcard *.c))
# 上面的依赖会在这里匹配,如main.o与%.o匹配,所以%.c就成了main.c,相当于main.o: main.c
%.o: %.c
	gcc -c $<
# 删除当前目录下所有以.o结尾的文件
clean:
	rm -f *.o
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3.2 目标依赖

默认目标:一个Makefile文件里通常会有多个目标,一般会选择第一个作为默认目标。

多目标:一个规则中也可以有多个目标,多个目标具有相同的生成命令和依赖文件。如一个目标文件%.o都是由其对应的源文件%.c编译生成的,生成命令也是相同的。

%.o: %.c
	gcc -o %.o %.c
  • 1
  • 2

多规则目标:多个规则可能是同一个目标,make在解析Makefile文件时,会将具有相同目标的规则的依赖文件合并。如果每个相同目标后跟一个冒号:,则多个目标只能有一个目标有执行命令,否则会报错;如果每个相同目标后跟双冒号::,则多个目标能有多个执行命令。

# 单冒号,只能一个目标有命令
helloworld: main.o
	cc -o helloworld $(OBJECTS)
helloworld: print.o

# 双冒号,多个目标可以有命令
blah::
	@echo "hello"
blah::
	@echo "hello again"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

伪目标:使用.PHONY表示目标是一个伪目标。伪目标一般没有依赖关系,也不会生成对应的目标文件,可以无条件执行,纯粹是为了执行某一个命令,如clean执行清理工作。

.PHONY : clean
clean:
        -rm helloworld $(OBJECTS)
  • 1
  • 2
  • 3

头文件依赖

make会根据时间戳来判断一个规则中的目标依赖文件是否有更新。make在编译程序时,会依次检查依赖关系树中的所有源文件的时间戳,如果发现某个文件的时间戳有更新,会认为这个文件有改动过,会重新编译这个源文件。如果发现文件的时间戳没有更新,就不会再重新编译一次。

在Makefile的规则中,一般不会把头文件添加到目标依赖中。当一个.c文件中包含多个头文件时,如果对应的头文件发生了变化,因为头文件没有包含在依赖关系树中,所以这个.c文件就不会重新编译。如我们的 1.5 的Makefile,修改print.h文件,并不会重新helloworld。有两种方式解决这个问题:

  1. 手动将头文件添加到规则中,一般不采取。
  2. 一个更高效的解决方法是:使用gcc -M命令自动生成头文件依赖关系。

四、条件判断

4.1 ifeq、ifneq 判断条件是否相等

ifeq关键字用来判断两个参数是够相等,相等时条件成立为true,不相等为false。ifeq一般和变量结合使用:

mode = debug

all:
ifeq ($(mode),debug)
	@echo "debug mode" 
else
	@echo "release mode"
endif
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

ifneq 关键字和ifeq关键字恰恰相反,用来判断参数是否不相等。当比较的参数不相等时,条件语句才成立,值为true,否则为false。

4.2 ifdef、ifndef 判断变量值是否为空

ifdef关键字用来判断一个变量是否已经定义,如果变量的值非空(在Makefile中,没有定义的变量的值为空),表达式为true。

mode = debug

all:
ifdef mode
	@echo "def mode" 
else
	@echo "ndef mode"
endif
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

ifndef关键字和ifdef相反,如果一个变量没有定义,表达式为true。ifdefifndef后面直接跟变量名,不用引用符号。

五、函数

关于函数的使用格式,有以下需要注意的地方:

  • 函数主要分为两类:make内嵌函数和用户自定义函数。对于 GNU make内嵌的函数,直接引用就可以了;对于用户自定义的函数,要通过make的call函数来间接调用。
  • 函数和参数列表之间要用空格隔开,多个参数之间使用逗号隔开(没有空格)。
  • 如果在参数中引用了变量,变量的引用建议和函数引用使用统一格式:要么是一对小括号,要么是一对大括号。

make内嵌的函数调用语法如下:

$(<function> <arg1>,<arg2>,...)
# 或者
${<function> <arg1>,<arg2>,...}
  • 1
  • 2
  • 3

5.1 字符串处理函数

GNU make提供了一系列文本处理函数:

  1. $(subst old,new,text)
    subst函数用来实现字符串的替换,将字符串text中的old替换为new
  2. $(patsubst pattern,replacement,text)
    patsubst函数用来做模式替换:查找text中的单词(单词以"空格",“tab”,"换行"来分割)是否符合pattern,符合的话,用replacement替代。
  3. $(strip text)
    strip函数用来将多个连续的空字符合并成一个,包括字符串开头、末尾的空字符。空字符包括:空格、多个空格、tab等不可显示的字符。
  4. $(findstring find,text)
    findstring函数会在字符串text中查找"find"字符串,如果找到,则返回字符串find,否则,返回空。
  5. $(filter pattern…,text)
    filter函数用来过滤掉字符串text中所有不符合pattern模式的单词,只留下符合pattern格式的单词。
  6. $(filer-out pattern…,text)
    filer-out函数是一个反过滤函数,功能和filter函数恰恰相反:该函数会过滤掉所有符合pattern模式的单词,保留所有不符合此模式的单词
  7. $(sort text)
    sort函数对字符串LIST中的单词以首字母为准进行排序,并删除重复的单词。
  8. $(word n,text)
    word函数从字符串text中,取出第n个单词。n大于字符串中单词的个数,返回空;如果n为0,则出错。
  9. $(wordlist n,m,text)
    wordlist函数用来从一个字符串text中取出第[n,m]个单词之间的一个单词串,n和m都是从1开始的一个数字。
  10. $(words text)
    words函数用来统计一个字符串text中单词的个数。
  11. $(firstword text)
    firstword函数用来取一个字符串中的首个单词,相当于$(word 1,text)
STR = a.c b.h c.s d.cpp
.PHONY: all
all:
	@echo $(subst not,totally,I am not superman)
	@echo $(patsubst %.c,%.o,$(wildcard *.c))
	@echo $(strip      hello	world  )
	@echo $(findstring hello,hello world)
	@echo $(filter %.c,$(STR))
	@echo $(filter-out %.c,$(STR))
	@echo $(sort $(STR))
	@echo $(word 2,$(STR))
	@echo $(wordlist 2,4,$(STR))
	@echo $(wordlist 2,4,$(STR))
	@echo $(words $(STR))
	@echo $(firstword $(STR))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

5.2 文件名处理函数

GNU make提供了一系列对文件名进行各种操作的函数:文件名替换、加前缀、去目录等。

  1. $(dir NAMES…)
    dir函数:取路径名的目录。dir函数会从NAMES文件名序列中,取出各个文件路径名中的目录部分并返回。
  2. $(notdir NAMES…)
    notdir函数:取文件名。
  3. $(suffix NAMES…)
    suffix函数:取文件名后缀。文件名的后缀是文件名中以点号.开始(包括点号)的部分。若文件名没有后缀,suffix函数则返回空。
  4. $(basename NAMES…)
    basename函数:取文件名前缀。
  5. $(addsuffix SUFFIX,NAMES…)
    addsuffix函数:给文件名加后缀。给文件列表中的每个文件名添加后缀SUFFIX。
  6. $(addprefix PREFIX,NAMES…)
    addprefix函数:给文件名加前缀
  7. $(join LIST1,LIST2)
    join函数的作用是:将字符串LIST1和字符串LIST2的各个单词依次连接,合并为新的单词构成的字符串。这是将字符串中每个对应位置上的单词连接,而不是连接字符串。
  8. $(wildcard PATTERN)
    wildcard函数的作用是:列出当前目录下所有符合PATTREN模式的文件名。
FILE_PATH := /home/loongson/workspace/makefile-test/main.c 
FILE_PATH += $(FILE_PATH)

.PHONY: all
all:
	@echo $(dir $(FILE_PATH))
	@echo $(notdir $(FILE_PATH))
	@echo $(suffix $(FILE_PATH))
	@echo $(basename $(notdir $(FILE_PATH)))
	@echo $(addsuffix .o,$(basename $(notdir $(FILE_PATH))))
	@echo $(addprefix test,$(suffix $(notdir $(FILE_PATH))))
	@echo $(join $(basename $(notdir $(FILE_PATH))),$(suffix $(FILE_PATH)))
	@echo $(wildcard *.c)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

5.3 foreach 函数

如果想做一些循环或遍历操作时,可以使用foreach函数:

$(foreach var,list,test)
  • 1

foreach函数的工作过程是:把list中使用空格分割的单词依次取出并赋值给变量var,然后执行text表达式。重复这个过程,直到遍历完list中的最后一个单词。函数的返回值是text多次计算的结果。

# 找出dirs所有目录下的所有.c文件
.PHONY: all
dirs = hello-demo test
srcs = $(foreach dir, $(dirs), $(wildcard $(dir)/*.c))
all:
	@echo $(srcs)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

5.4 if 函数

if函数提供了在一个函数上下文中实现条件判断的功能,类似于ifeq关键字,if函数的使用格式如下:

$(if CONDITION,THEN-PART)
$(if CONDITION,THEN-PART[,ELSE-PART])
  • 1
  • 2

if 函数的第一个参数 CONDITION表示条件判断,展开后如果非空,则条件为真,执行 THEN-PART部分;否则,如果有ELSE-PART部分,则执行ELSE-PART部分。

if函数的返回值即执行分支(THEN-PARTELSE-PART)的表达式值。如果没有ELSE-PART,则返回一个空字符串。

# 指定安装路径,默认则是/usr/local
.PHONY: all
install_path =
all:
	@echo $(if $(install_path),$(install_path),/usr/local)
  • 1
  • 2
  • 3
  • 4
  • 5

5.5 call 函数

用户自定义函以define开头,endef结束,给函数传递的参数在函数中使用$(0)$(1)引用,分别表示第1个参数、第2个参数…

使用call函数可以用来间接调用用户自定义函数,各个参数之间使用空格隔开:

.PHONY: all
define func
    @echo "pram1 = $(0)"
    @echo "pram2 = $(1)"
endef
all:
    $(call func, hello zhaixue.cc)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

call函数不仅可以用来调用一个用户自定义函数并传参,还可以向一个表达式传参:$(call <expression>,<parm1>,<parm2>,<parm3>...)

.PHONY: all
param = $(1) $(2)
str1 = $(call param, hello, zhaixue.cc)
all:
	@echo $(str1)
  • 1
  • 2
  • 3
  • 4
  • 5

5.6 origin 函数

origin函数的使用格式为:$(origin <variable>)

如果变量没有定义,origin函数的返回值为:undefined,不同的返回值代表变量的类型不同。常见的返回值如下:

  • default:变量是一个默认的定义,比如 CC 这个变量。
  • file:这个变量被定义在Makefile中。
  • command line:这个变量是被命令行定义的。
  • override:这个变量是被override指示符重新定义过的。
  • automatic:一个命令运行中的自动化变量。
.PHONY: all
WEB = www.zhaixue.cc
web_type = $(origin WEB)
all:
	@echo $(origin WEB)
	@echo $(origin CC)
	@echo $(origin CMD)

# make
# make CMD=pwd
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

5.7 shell 函数

如果你想在Makefile中运行shell命令,可以使用shell函数来完成这个功能。shell函数的参数是shell命令,它和反引号具有相同的功能。shell命令的运行结果即为shell函数的返回值。

.PHONY: all
all:
	@echo $(shell pwd)
	@echo $(shell ls -m)
  • 1
  • 2
  • 3
  • 4

5.8 error 和 warning 函数

make提供了两个可以控制make运行方式的函数:errorwarning。两个函数都会产生错误提示信息,但是error会终止make的运行,而warning则不会。

.PHONY: all
all:
	@echo "make command start..."
	$(warning find a error)#只发出提示信息
	$(error find a error)#发出提示信息,并终止make运行
	@echo "make command end..."
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/641024
推荐阅读
相关标签
  

闽ICP备14008679号