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)

