HVAC_M7_CLIMATIC/modular.py

667 lines
29 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/python3
import subprocess
import sys
import argparse
import os
import json
import platform
import re
import shutil
from datetime import datetime
def get_git_revision_hash(path=None) -> str:
if path:
# git -C <path> <command>
# Runs the command as if git was started in <path> instead of the current working directory.
# return subprocess.check_output(['git', '-C', path, 'rev-parse', 'HEAD']).decode('ascii').strip()
stdout = subprocess.run(['git', '-C', path, 'rev-parse', 'HEAD'],
check=True, capture_output=True, text=True).stdout.strip()
return stdout
else:
# return subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('ascii').strip()
stdout = subprocess.run(['git', 'rev-parse', 'HEAD'],
check=True, capture_output=True, text=True).stdout.strip()
return stdout
class DependencyType:
local = 'local'
git = 'git'
class Module:
# raw - это прочитанный json в виде словаря
def __init__(self, raw=None):
self.cmake = {'inc': [], 'src': [], 'inc_dirs': [], 'srcs': [], 'libs': [], 'uis': []}
self.dep = [] # список из объектов Dependency
self.raw = raw
if raw:
if 'cmake' in raw:
if 'inc_dirs' in raw['cmake']:
self.cmake['inc_dirs'] = raw['cmake']['inc_dirs']
if 'srcs' in raw['cmake']:
self.cmake['srcs'] = raw['cmake']['srcs']
if 'libs' in raw['cmake']:
self.cmake['libs'] = raw['cmake']['libs']
if 'uis' in raw['cmake']:
self.cmake['uis'] = raw['cmake']['uis']
if 'dep' in raw:
self.dep = [Dependency(rawDep) for rawDep in raw['dep']]
def __str__(self):
return str(self.cmake)
def __repr__(self):
return self.__str__()
def __eq__(self, other):
if not isinstance(other, type(self)):
return False
return self.cmake == other.cmake and self.dep == other.dep
class Dependency:
# raw - это словарь
def __init__(self, raw):
if raw:
if raw['type'] == DependencyType.local:
self.type = DependencyType.local
self.dir = raw.get('dir', '')
self.os = raw.get('os', '')
elif raw['type'] == DependencyType.git:
self.type = DependencyType.git
self.provider = raw.get('provider', '')
self.dir = raw.get('dir', '')
self.repo = raw.get('repo', '')
self.commit = raw.get('commit', '')
self.os = raw.get('os', '')
self.project = raw.get('project', 'false')
else:
raise ValueError('Unknown dependency ' + str(raw['type']))
def __eq__(self, other):
if self.type != other.type:
return False
if self.type == DependencyType.local:
return self.dir == other.dir
elif self.type == DependencyType.git:
return self.repo == other.repo
else:
raise ValueError('Unknown dependency' + str(self.type))
def __str__(self):
if self.type == DependencyType.local:
return 'local ' + str(self.dir)
elif self.type == DependencyType.git:
return 'git ' + str(self.repo)
else:
raise ValueError('Unknown dependency' + str(self.type))
class ModularProject:
# получить домашнюю директорию и приклеить к ней путь к modular_builder/config.json
userConfigFile = os.path.join(os.path.expanduser('~'), '.modular_builder', 'config.json')
def __init__(self):
self.clear()
self.initDir()
self.loadConfigs()
def clear(self):
self.loadedModules = []
self.rules = {}
def initDir(self):
if not os.path.exists('MODULES'):
print('No MODULES folder, creating...')
os.mkdir('MODULES')
print('Done')
def loadConfigs(self):
if os.path.exists(self.userConfigFile):
print('Has config file at ' + str(self.userConfigFile) + ', loading...')
self.loadConfig(self.userConfigFile)
print('Done')
else:
print('No config file at ' + str(self.userConfigFile))
# в config.json лежит примерно это:
# {
# "git_providers:" {
# "rospron_modules": "http"//git...blabla.../rosprom_modules"
# }
# }
def loadConfig(self, config_path):
config = json.load(open(config_path, encoding='utf-8'))
self.gitProviders = config['git_providers']
def loadJsonAtPath(self, path): # path - путь до modular.json
print('loading json', path)
try:
with open(path, encoding='utf-8') as f:
result = Module(json.load(f))
return result
except json.JSONDecodeError:
print('Error while reading json file at path:', path, file=sys.stderr)
print('Try fixing JSON file and run modular.py again', file=sys.stderr)
raise json.JSONDecodeError
def downloadableModulePath(self, dependency):
if dependency.type == DependencyType.git:
if dependency.project == 'true':
return dependency.repo
else:
return os.path.join('MODULES', dependency.repo)
raise ValueError('UnknownDependency')
def isGitModuleCloned(self, dependency):
return os.path.exists(self.downloadableModulePath(dependency))
def isModuleSystemAppropriate(self, dependency):
if not dependency.os: # если ОС не указана для модуля, то грузим его всегда
return True
elif dependency.os.lower() == 'windows':
print('Module only for Windows')
return platform.system().lower() == 'windows'
elif dependency.os.lower() == 'linux':
print('Module only for Linux')
return platform.system().lower() == 'linux'
else:
print('Unknown OS:', dependency.os)
print('Dependency will be uploaded anyway')
return True
def cloneGitDependency(self, dependency):
path = self.downloadableModulePath(dependency)
clone_cmd = 'git clone ' + str(self.gitProviders[dependency.provider]) + '/' + str(dependency.repo) + ' ' + str(
path)
print('Clone with', clone_cmd)
os.system(clone_cmd) # выполняет любые поддерживаемые команды в командной строке
if dependency.commit:
checkout_cmd = f'git -C {path} checkout {dependency.commit}'
# git -C <path> <command>
# Runs the command as if git was started in <path> instead of the current working directory.
print(f'Checkout with {checkout_cmd}')
os.system(checkout_cmd) # выполняет любые поддерживаемые команды в командной строке
def loadDependency(self, dependency):
if self.checkDependencyCollision(dependency):
return []
print('\nLoading module', dependency)
if dependency.type == DependencyType.local:
return self.loadDependencyFromLocal(dependency)
elif dependency.type == DependencyType.git:
return self.loadDependencyFromGit(dependency)
else:
raise ValueError('Unknown module')
def loadDependencyFromLocal(self, dependency):
if self.isModuleSystemAppropriate(dependency):
return self.loadDependencyFromDir(dependency.dir)
def loadDependencyFromGit(self, dependency):
if self.isModuleSystemAppropriate(dependency):
if not self.isGitModuleCloned(dependency):
self.cloneGitDependency(dependency)
return self.loadDependencyFromDir(self.downloadableModulePath(dependency))
def loadDependencyFromDir(self, modular_dir):
modular_json_path = os.path.join(modular_dir, 'modular.json')
if not os.path.exists(modular_json_path):
print('Directory in not a module:', modular_dir)
return []
# module_relative - объект класса Module, внутри него в списке dep хранятся прочитанные из json объекты Dependency
module_relative = self.loadJsonAtPath(modular_json_path)
project_relative = Module()
print('module_relative:', module_relative)
print(module_relative.cmake)
result_inc_dirs = self.addRootToPaths(modular_dir, module_relative.cmake.get('inc_dirs', ''))
project_relative.cmake['inc_dirs'] = result_inc_dirs
result_srcs = self.addRootToPaths(modular_dir, module_relative.cmake.get('srcs', ''))
project_relative.cmake['srcs'] = result_srcs
result_libs = self.addRootToPaths(modular_dir, module_relative.cmake.get('libs', ''))
project_relative.cmake['libs'] = result_libs
result_uis = self.addRootToPaths(modular_dir, module_relative.cmake.get('uis', ''))
project_relative.cmake['uis'] = result_uis
# После выполнения строки ниже в project_relative.dep окажется просто
# список Dependency, который и был в module_relative.dep.
# (Это список модулей, от которых текущий модуль зависит. Их надо будет докачать)
project_relative.dep = self.addRootToDeps(modular_dir, module_relative.dep)
sub_modules = []
for dependency in project_relative.dep:
loaded_dependencies = self.loadDependency(dependency)
if loaded_dependencies:
# sub_modules.extend(loaded_dependencies)
# чтобы не было повторений
for dep_loaded in loaded_dependencies:
if dep_loaded not in sub_modules:
sub_modules.append(dep_loaded)
else:
print('Nothing to download, continuing')
sub_modules.append(project_relative)
print('sub_modules', sub_modules)
return sub_modules
def addRootToPaths(self, root, paths):
if not paths:
return []
result = []
for pth in paths:
if os.path.isabs(pth): # если путь абсолютный
result.append(pth)
else:
if pth.startswith('./'): # если текущий путь начинается с текущей папки, то убери ее
pth = pth[2:]
result.append(os.path.join(root, pth))
return result
# эта функция возвращает просто список объектов Dependency
def addRootToDeps(self, root, deps):
result = []
for dep in deps:
if dep.type == DependencyType.local:
if not os.path.isabs(dep.dir):
dep.dir = os.path.join(root, dep.dir)
result.append(dep)
return result
def download(self):
# self.modules - это список из объектов Dependency
self.modules = self.loadDependency(Dependency({'type': 'local', 'dir': './'}))
print('modules: ')
print(self.modules)
def replace_slashes(self, path):
return path.replace('\\', '/')
def genCmake(self):
cmakeText = '''function(add_sources FILE_LIST FILES_PATH)
file(GLOB_RECURSE ADD_FILES_LIST ${FILES_PATH})
list(APPEND ${FILE_LIST} ${ADD_FILES_LIST})
set(${FILE_LIST} ${${FILE_LIST}} PARENT_SCOPE)
endfunction()\n\n'''
for module in self.modules:
for inc in module.cmake['inc_dirs']:
cmakeText += 'include_directories("' + str(inc) + '")\n'
cmakeText += '\n'
for module in self.modules:
for src in module.cmake['srcs']:
cmakeText += 'add_sources(SOURCES "' + str(src) + '")\n'
cmakeText += '\n'
for module in self.modules:
for lib in module.cmake['libs']:
cmakeText += 'add_sources(LIBRARIES "' + str(lib) + '")\n'
cmakeText += '\n'
for module in self.modules:
for ui in module.cmake['uis']:
cmakeText += 'add_sources(UIS "' + str(ui) + '")\n'
cmakeText += '\n'
# заменяем обратный слеш на прямой, так как винда склеивает своими слешами, но их не поинмает компилятор:
cmakeText = self.replace_slashes(cmakeText)
print(cmakeText)
with open('modular.cmake', 'w', encoding='utf-8') as file:
file.write(cmakeText)
def genCmakeForLibrify(self):
other_path, project_name = os.path.split(os.getcwd())
cmakeText = '''cmake_minimum_required(VERSION 3.17)
project(''' + project_name + ''')
set(CMAKE_C_STANDARD 11)
function(add_sources FILE_LIST FILES_PATH)
file(GLOB ADD_FILES_LIST ${FILES_PATH})
list(APPEND ${FILE_LIST} ${ADD_FILES_LIST})
set(${FILE_LIST} ${${FILE_LIST}} PARENT_SCOPE)
endfunction()\n\n'''
for module in self.modules:
for inc in module.cmake['inc_dirs']:
cmakeText += 'include_directories("' + str(inc) + '")\n'
cmakeText += '\n'
module = self.modules[-1] # берем только главный (корневой) модуль
for src in module.cmake['srcs']:
cmakeText += 'add_sources(SOURCES "' + str(src) + '")\n'
cmakeText += '\n'
for module in self.modules:
for lib in module.cmake['libs']:
cmakeText += 'add_sources(LIBRARIES "' + str(lib) + '")\n'
cmakeText += '\n'
for module in self.modules:
for ui in module.cmake['uis']:
cmakeText += 'add_sources(UIS "' + str(ui) + '")\n'
cmakeText += '\n'
cmakeText += 'add_library(' + project_name + ' ${SOURCES})\n'
# заменяем обратный слеш на прямой, так как винда склеивает своими слешами, но их не поинмает компилятор:
cmakeText = self.replace_slashes(cmakeText)
print(cmakeText)
with open('modular.cmake', 'w', encoding='utf-8') as file:
file.write(cmakeText)
def checkDependencyCollision(self, dependency):
"""Возвращает True, если зависимость dependency уже есть среди загруженных модулей"""
# TODO проверка не работает, в self.loadedModules ничего не добавляется, коллизии происходят, ну и пофиг...
return any([loaded == dependency for loaded in self.loadedModules])
# def genEclipseXmlPart(self) - эта функция использоваться не будет
# def genExp32Cmake(self) - эта функция использоваться не будет
def load_rules(self, config_path):
try:
rules = json.load(open(config_path, encoding='utf-8'))
except json.JSONDecodeError:
print('Error while reading json file at path:', config_path, file=sys.stderr)
print('Try fixing JSON file and run modular.py again', file=sys.stderr)
raise json.JSONDecodeError
if 'replacements' not in rules:
rules['replacements'] = {}
if 'filetypes' not in rules:
rules['filetypes'] = [".c", ".cpp", ".h"] # если типы файлов не указали, будем менять только в этих
if 'remove_comments' not in rules:
rules['remove_comments'] = False
if 'librify' not in rules:
rules['librify'] = []
if 'ignore' not in rules:
rules['ignore'] = []
self.rules = rules
def replacements_and_comments_deletion(self):
project_path = '.'
def remove_comments_in_text(string):
pattern = r"(\".*?\"|\'.*?\')|(/\*.*?\*/|//[^\r\n]*$)"
# первая группа захватывает строки в кавычках (двойных или одинарных)
# вторая группа захватывает комментарии (
regex = re.compile(pattern, re.MULTILINE | re.DOTALL)
def _replacer(match):
# если вторая группа (захватывающая комменты) не None,
# значит, мы захватили не закавыченные (нормальные) комменты
if match.group(2) is not None:
return "" # возвращаем пустую строку для удаления коммента
else: # иначе возвращаем 1-ую группу
return match.group(1) # поймали "закавыченную" строку
return regex.sub(_replacer, string)
def remove_comments_in_file(filename):
with open(filename, 'r') as f:
content = f.read()
content_new = remove_comments_in_text(content)
with open(filename, 'w') as f:
f.write(content_new)
def should_exclude_folder(project_path, dirpath, rules):
folder_fullpath = os.path.normpath(dirpath)
return any([folder_fullpath == os.path.normpath(os.path.join(project_path, ignore_path))
for ignore_path in rules['ignore']])
def replace_occurrences_in_file(filename, rules):
with open(filename, 'r') as f:
content = f.read()
content_new = content
for (old_name, new_name) in rules['replacements'].items():
content_new = re.sub(old_name, new_name, content_new)
with open(filename, 'w') as f:
f.write(content_new)
for (dirpath, dirnames, filenames) in os.walk(project_path):
# print('*****', dirpath, dirnames, filenames)
if should_exclude_folder(project_path, dirpath, self.rules):
# если папка входит в игнорируемые, то пропускаем
print('Исключена папка:', dirpath)
continue
for file in filenames:
filename, file_extension = os.path.splitext(file)
if file_extension not in self.rules['filetypes']:
# если расширение не входит в список заданных, то пропускаем этот файт
continue
fullpath = os.path.join(dirpath, file)
print(fullpath)
if self.rules['remove_comments']:
remove_comments_in_file(fullpath)
replace_occurrences_in_file(fullpath, self.rules)
def libraryfication(self):
initial_wd = os.getcwd()
"""Собирает библиотеки из модулей в папках, указанных в self.rules["librify"] """
if not os.path.exists('SHRINK'):
print('No SHRINK folder, creating...')
os.mkdir('SHRINK')
print('Done')
else:
print(
'SHRINK folder already exists. Stopping. \n(If you want to generate libs, delete SHRINK folder first)')
return
cmake_lists_text = '''include(../arch.cmake)
function(add_sources FILE_LIST FILES_PATH)
file(GLOB_RECURSE ADD_FILES_LIST ${FILES_PATH})
list(APPEND ${FILE_LIST} ${ADD_FILES_LIST})
endfunction()
include(modular.cmake)
'''
for directory in self.rules["librify"]:
if not os.path.exists(directory):
print('Error! Directory from shrink.json does not exists:', directory)
raise FileNotFoundError
os.chdir(directory) # переходим в папку к этому модулю
library_name = directory.split('/')[-1]
config = json.load(open('modular.json', encoding='utf-8'))
for src in config['cmake']['srcs']:
cmake_lists_text += f'add_sources({library_name}_SOURCES "' + '../MODULES/' + library_name + '/' + src + '")\n'
cmake_lists_text += 'add_library(' + library_name + ' ${' + library_name + '_SOURCES})'
os.chdir(initial_wd) # в конце не забываем вернуться в корневую папку проекта
os.chdir('SHRINK')
with open('CMakeLists.txt', 'w', encoding='utf-8') as file:
file.write(cmake_lists_text)
with open('modular.json', 'w') as new_modular:
modular_text = '''{
"dep": [
{
"type": "local",
"dir": "../"
}
]
}'''
new_modular.write(modular_text)
result_run = subprocess.run(["modular.py", "-c"])
os.mkdir('build')
os.chdir('build')
result_run = subprocess.run(["cmake", ".."])
if result_run.returncode != 0:
print('cmake not successful. Stop')
raise RuntimeError('Previous cmake not successful. Stop')
result_run = subprocess.run(["make"])
if result_run.returncode != 0:
print('make not successful. Stop')
raise RuntimeError('Previous make not successful. Stop')
# print('Result code of cmake:', result_run.returncode)
os.chdir('..')
os.chdir(initial_wd)
self.copy_all_libs_to_libs_folder()
def copy_all_libs_to_libs_folder(self):
current_wd = os.path.abspath(os.getcwd())
try:
if os.path.exists('libs'):
print('libs already exists')
else:
os.mkdir('libs')
dest_path = os.path.join(current_wd, 'libs')
os.chdir('SHRINK')
os.chdir('build')
for (dirpath, dirnames, filenames) in os.walk('.'):
for file in filenames:
if file.endswith(".a"):
source_file_path = os.path.join(dirpath, file)
shutil.copy(source_file_path, dest_path)
except FileNotFoundError as e:
print('Couldn\'t copy .a libs, error occurred!')
print(e)
os.chdir(current_wd) # возвращаемся в исходную папку
def delete_sources_of_libraries(self):
current_wd = os.path.abspath(os.getcwd())
for module_directory in self.rules["librify"]:
if not os.path.exists(module_directory):
print('Error! Directory from shrink.json does not exists:', module_directory)
raise FileNotFoundError
os.chdir(module_directory)
config = json.load(open('modular.json', encoding='utf-8'))
needed_dirs = config['cmake']['inc_dirs']
all_dirs_files = [name for name in os.listdir(os.getcwd()) if
os.path.isdir(os.path.join(os.getcwd(), name))]
print('all files:', all_dirs_files)
for dir in all_dirs_files:
if dir not in needed_dirs:
print(f'remove dir {dir} in {os.path.join(current_wd, module_directory)}')
shutil.rmtree(dir)
os.chdir(current_wd)
def delete_all_modular_json_files(self):
print('Starting to delete all modular.json...')
for (dirpath, dirnames, filenames) in os.walk('.'):
for file in filenames:
if str(file) == 'modular.json':
os.remove(os.path.join(dirpath, file))
print('All modular.json files deleted from the project!')
def shrink(self):
print('Start shrinking...')
self.load_rules('shrink.json')
self.replacements_and_comments_deletion()
self.libraryfication()
self.delete_sources_of_libraries()
self.delete_all_modular_json_files()
print('Done shrinking')
def create_release(self, tag=None):
"""
Сохраняет информацию о текущих версиях каждого из модулей в качестве релиза.
В результате в корне проекта создается файл с именем <dd.mm.yyyy_hh.mm[_tag]_release>
:param tag:
:return:
"""
# without_duplicates = []
# for module in self.modules:
# if module not in without_duplicates:
# without_duplicates.append(module)
# print(without_duplicates)
dependencies_without_duplicated = []
for module in self.modules:
if module.dep:
for dep in module.dep:
if dep not in dependencies_without_duplicated:
dependencies_without_duplicated.append(dep)
# print(dependencies_without_duplicated)
all_dependencies_for_project = {"dep": []}
for dep in dependencies_without_duplicated:
try:
path = os.path.join('MODULES', dep.repo)
commit_hash = get_git_revision_hash(path)
dep_json = {
"type": dep.type,
"provider": dep.provider,
"repo": dep.repo,
"commit": commit_hash
}
all_dependencies_for_project['dep'].append(dep_json)
except AttributeError as e:
pass # игнорим: 'Dependency' object has no attribute 'repo' (dep=local ./APP)
except Exception as e:
print(f'Случилось испключение: {e} (dep={dep})')
# print(all_dependencies_for_project)
release_file_name = datetime.today().strftime('%d.%m.%Y_%H.%M') # <dd.mm.yyyy_hh.mm_release[_tag]>
if tag:
release_file_name += '_' + tag
release_file_name += '_release.json'
with open(release_file_name, 'w', encoding='utf-8') as output_file:
json.dump(all_dependencies_for_project, output_file, ensure_ascii=False, indent=2)
print(f'Создан файл релиза: {release_file_name}')
def checkout_release(self, json_name):
# with open(json_name, encoding='utf-8') as f:
# release_json = json.load(f)
print('Начинаем переход на указанные коммиты в релизе...')
module = self.loadJsonAtPath(json_name)
for dependency in module.dep:
if dependency.commit:
path = self.downloadableModulePath(dependency)
checkout_cmd = f'git -C {path} checkout {dependency.commit}'
# git -C <path> <command>
# Runs the command as if git was started in <path> instead of the current working directory.
print(f'Checkout with {checkout_cmd}')
os.system(checkout_cmd) # выполняет любые поддерживаемые команды в командной строке
print(f'Вернули состояние проекта к релизной версии {json_name}')
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--cmake', action='store_true', help='require generate cmake from modules')
parser.add_argument('-s', '--shrink', action='store_true',
help='Согласно правилам из shrink.json выполняет замены по RegEx и при "remove_comments": true удаляет все комментарии из кода')
parser.add_argument('-d', '--download', action='store_true', help='require download submodules')
parser.add_argument('wdir', help='path to working directory', type=str, default='./', nargs='?')
parser.add_argument('-r', '--release', action='store_true',
help='Отмечает текущее состояние проекта как релизное, создает соответствующий состоянию json')
parser.add_argument('--checkout', type=str,
help='Переходит в указанный релиз (выполняет checkout). Для релиза должен быть заранее создан релизный json')
argv = parser.parse_args()
print('working directory', argv.wdir)
os.chdir(argv.wdir) # изменить текущую рабочую директорию
project = ModularProject()
if argv.download or argv.cmake or argv.shrink:
project.download()
if argv.cmake:
project.genCmake()
if argv.shrink:
project.shrink()
if argv.release:
print('release')
project.download()
project.create_release()
if argv.checkout:
project.download()
project.checkout_release(argv.checkout)
# TODO: после checkout проекты будут в состоянии detached HEAH (оторванный указатель HEAD),
# и надо уметь какой-то командой возвращаться в исходную позицию (с учитыванием commit, указанного в самом проекте...)