Scons - makefile w pythonie
Kilka miesięcy temu szukałem alternatywy do automatycznie generowanych makefilie i natrafiłem na projekt o nazwie scons.
Użyłem tego projektu do kompilacji jednej z moich aplikacji. Największym plusem tego rozwiązania dla mnie był, to że nie trzeba uczyć się nowego "języka" do pisania skryptów kompilacyjnych. Pythona znałem więc nie miałem z tym problemu.
Wygląd pliku
Poniżej znajdują się proste funkcje pomocnicze, które ułatwiają przygotowanie środowiska kompilacyjnego.
# tworzenie listy podkatalogów def getSubdirs(abs_path_dir): lst = [ name for name in os.listdir(abs_path_dir) if os.path.isdir(os.path.join(abs_path_dir, name)) and name[0] != '.' ] lst.sort() return lst # wyszukiwanie plikow zrodlowy cpp i hpp def getSources(src_dir): sources = [] for root, dirnames, filenames in os.walk(src_dir): for filename in fnmatch.filter(filenames, '*.cpp'): sources.append(str(os.path.join(root, filename))) for filename in fnmatch.filter(filenames, '*.hpp'): sources += Glob(os.path.join(root, filename)) return sources # sprawdzenie czy dana komenda istnieja def cmd_exists(cmd): return subprocess.call("type " + cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0 # funkcja do przygotowania zmiennych srodowiskowych def prepare_env(env_vars): env = {} for var in env_vars: if var in os.environ: env[var] = os.environ[var] return env
Dalej widać przygotowanie zmiennych do kompilacji
num_cpu = int(os.environ.get('NUM_CPU', 2)) src_dir = 'src' atabox_sources = getSources(src_dir) modules = getSubdirs(src_dir) print('modules {}'.format(modules)) cpp_defined = {'DEBUG': 1, 'BOOST_LOG_DYN_LINK': 'yes'} common_libs = [ 'pthread', 'boost_system', 'boost_thread', 'boost_iostreams', 'boost_filesystem', 'boost_log', 'rocksdb', 'bz2', 'z', 'cpprest', 'Aquila', 'Ooura_fft' ] libs_path = ['external/aquila/lib'] CXX = 'g++' # w przypadku gdy zainstalowany jest clang to go uzyj if cmd_exists('clang++'): CXX = 'clang++' SetOption('num_jobs', num_cpu) print "running with -j", GetOption('num_jobs')
Przygotowanie zmiennych zawierających:
- liczbę CPU użytych do kompilacji
- pliki źródłowe
- pliki nagłówkowe
- stałe przekazane do kompilatora
- linkowane biblioteki
- i na końcu wybór kompilatora
Dalej wszystkie te zmienne są przekazane do Env Scons
env_env = prepare_env(['PATH','TERM', 'HOME']) VariantDir(build_dir, '.') env = Environment(ENV = env_env, CXX = CXX ) env.Append(CPPPATH=include_dir) env.Append(CPPFLAGS=cppflags) env.Append(CPPDEFINES=cpp_defined) env.Append( LIBS = common_libs ) env.Append(LIBPATH = libs_path)
Dodatkowo przekazuję też obecny env aby wynik kompilacji był kolorowany.
Całość skryptu wygląda następująco:
import os import fnmatch import subprocess def getSubdirs(abs_path_dir): lst = [ name for name in os.listdir(abs_path_dir) if os.path.isdir(os.path.join(abs_path_dir, name)) and name[0] != '.' ] lst.sort() return lst def getSources(src_dir): sources = [] for root, dirnames, filenames in os.walk(src_dir): for filename in fnmatch.filter(filenames, '*.cpp'): sources.append(str(os.path.join(root, filename))) for filename in fnmatch.filter(filenames, '*.hpp'): sources += Glob(os.path.join(root, filename)) return sources def cmd_exists(cmd): return subprocess.call("type " + cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0 def prepare_env(env_vars): env = {} for var in env_vars: if var in os.environ: env[var] = os.environ[var] return env num_cpu = int(os.environ.get('NUM_CPU', 2)) src_dir = 'src' atabox_sources = getSources(src_dir) modules = getSubdirs(src_dir) print('modules {}'.format(modules)) cpp_defined = {'DEBUG': 1, 'BOOST_LOG_DYN_LINK': 'yes'} common_libs = [ 'pthread', 'boost_system', 'boost_thread', 'boost_iostreams', 'boost_filesystem', 'boost_log', 'rocksdb', 'bz2', 'z', 'cpprest', 'Aquila', 'Ooura_fft' ] libs_path = ['external/aquila/lib'] CXX = 'g++' if cmd_exists('clang++'): CXX = 'clang++' SetOption('num_jobs', num_cpu) print "running with -j", GetOption('num_jobs') include_dir = [ "src", "external/aquila/aquila/", "external/casablanca/Release/include/", "external/rocksdb_source/include/", "external/kiss_fft", "external/" ] cppflags = ['--std=c++11', '-Wall', '-g', '-fdiagnostics-color=always'] output = 'atabox-server' build_dir = 'build' source_files = [os.path.join(build_dir, s) for s in atabox_sources] comp = ARGUMENTS.get('env', 'debug') if comp.startswith('tests'): print('---------------------_TEST_---------------------------') CXX = 'g++' common_libs.append('gcov') common_libs += ['gtest', 'gmock'] cppflags.append('-g3') cppflags += ['-c', '-fmessage-length=0', '-fprofile-arcs', '-ftest-coverage'] output = 'atabox-tests' atabox_sources.pop(0) source_files.pop(0) atabox_sources += getSources('tests') if 'jenkins' in comp: cppflags.remove('-Wall') cppflags.remove('-fdiagnostics-color=always') cppflags.append('-O0') cppflags.append('-w') source_files += [os.path.join(build_dir, s) for s in getSources('tests')] else: common_libs += ['boost_program_options'] env_env = prepare_env(['PATH','TERM', 'HOME']) VariantDir(build_dir, '.') env = Environment(ENV = env_env, CXX = CXX ) env.Append(CPPPATH=include_dir) env.Append(CPPFLAGS=cppflags) env.Append(CPPDEFINES=cpp_defined) env.Append( LIBS = common_libs ) env.Append(LIBPATH = libs_path) env.Program('build/' + output, source_files)
Oprócz tego co opisałem w przypadku przekazania parametru skrypt kompilacyjny może działać inaczej. Było mi to potrzebne do uruchomienia kompilacjie na Jenkinsie (dodatkowe flagi do pokrycia kodu testami i valgrinda)