From 210e2cd95a5b478011fb13853cf93816bbc6b47b Mon Sep 17 00:00:00 2001 From: Martin Siggel Date: Mon, 7 Mar 2022 07:54:00 +0100 Subject: [PATCH] Implemented custom directory structure This PR implements a custom directory structure for fbs. Note: the previous directory structure is still used by default. Implementation details ---------------------- A customized directory structure is implemented as follows: - Default directories are configured in _defaults/src/build/settings/base.json - If the file `fbs_directories.json` in the root directory exists it will be read by fbs. Here, custom paths can be set, with the following options - python_dir : Directory of the python code - icons_dir : Directory of the icons - resources_dir: Directory of other resources.py - settings_dir: Directory of the fbs settings - freeze_config_dir: Configuration files for the freeze command - Eventually, the remaining fbs settings are read (either from the default or user configured path) I had to alter the check for the root directory, as it assumed a fixed directory structure. Now, it will check for the default settings path or the existence of the fbs_directories.json file. Testing ------- I tested the functionality on the fbs test app, that is created by the fbs tutorial. I tested the commands `run`, `freeze` and `installer` successfully. I tested only on windows though. Mac and Linux are yet untested. Closes #268 --- fbs/__init__.py | 7 ++-- fbs/_defaults/src/build/settings/base.json | 7 +++- fbs/builtin_commands/__init__.py | 2 +- fbs/freeze/__init__.py | 4 +- fbs/freeze/linux.py | 2 +- fbs/freeze/windows.py | 6 +-- fbs/resources.py | 2 +- fbs/sign/windows.py | 2 +- fbs_runtime/_settings.py | 23 +++++++---- fbs_runtime/_source.py | 43 +++++++++++++++------ fbs_runtime/application_context/__init__.py | 2 +- 11 files changed, 65 insertions(+), 35 deletions(-) diff --git a/fbs/__init__.py b/fbs/__init__.py index 09b6d40..49f85fc 100644 --- a/fbs/__init__.py +++ b/fbs/__init__.py @@ -2,8 +2,8 @@ from fbs._state import LOADED_PROFILES from fbs_runtime import FbsError, _source from fbs_runtime._fbs import get_core_settings, get_default_profiles -from fbs_runtime._settings import load_settings, expand_placeholders -from fbs_runtime._source import get_settings_paths +from fbs_runtime._settings import expand_placeholders +from fbs_runtime._source import load_settings_from_paths from os.path import abspath import sys @@ -38,9 +38,8 @@ def activate_profile(profile_name): """ LOADED_PROFILES.append(profile_name) project_dir = SETTINGS['project_dir'] - json_paths = get_settings_paths(project_dir, LOADED_PROFILES) core_settings = get_core_settings(project_dir) - SETTINGS.update(load_settings(json_paths, core_settings)) + SETTINGS.update(load_settings_from_paths(project_dir, LOADED_PROFILES, core_settings)) def path(path_str): """ diff --git a/fbs/_defaults/src/build/settings/base.json b/fbs/_defaults/src/build/settings/base.json index f98b454..308757e 100644 --- a/fbs/_defaults/src/build/settings/base.json +++ b/fbs/_defaults/src/build/settings/base.json @@ -4,6 +4,11 @@ "src/unittest/python", "src/integrationtest/python" ], + "python_dir": "src/main/python", + "icons_dir": "src/main/icons", + "resources_dir": "src/main/resources", + "settings_dir": "src/build/settings", + "freeze_config_dir": "src/freeze", "files_to_filter": [ "src/build/docker/ubuntu/.bashrc", "src/build/docker/ubuntu/Dockerfile", "src/build/docker/arch/.bashrc", "src/build/docker/arch/Dockerfile", @@ -12,7 +17,7 @@ ], "hidden_imports": [], "extra_pyinstaller_args": [], - "public_settings": ["app_name", "author", "version", "environment"], + "public_settings": ["app_name", "author", "version", "environment", "icons_dir", "resources_dir"], "docker_images": { "ubuntu": { "build_files": ["requirements/", "src/sign/linux/"], diff --git a/fbs/builtin_commands/__init__.py b/fbs/builtin_commands/__init__.py index 4a23dbd..231b726 100644 --- a/fbs/builtin_commands/__init__.py +++ b/fbs/builtin_commands/__init__.py @@ -88,7 +88,7 @@ def run(): " pip install PySide2==5.12.2" ) env = dict(os.environ) - pythonpath = path('src/main/python') + pythonpath = path(SETTINGS['python_dir']) old_pythonpath = env.get('PYTHONPATH', '') if old_pythonpath: pythonpath += os.pathsep + old_pythonpath diff --git a/fbs/freeze/__init__.py b/fbs/freeze/__init__.py index 8a16641..72edd0f 100644 --- a/fbs/freeze/__init__.py +++ b/fbs/freeze/__init__.py @@ -81,5 +81,5 @@ def _generate_resources(): resources_dest_dir = freeze_dir for path_fn in default_path, path: for profile in LOADED_PROFILES: - _copy(path_fn, 'src/main/resources/' + profile, resources_dest_dir) - _copy(path_fn, 'src/freeze/' + profile, freeze_dir) \ No newline at end of file + _copy(path_fn, path('${resources_dir}/%s' % profile), resources_dest_dir) + _copy(path_fn, path('${freeze_config_dir}/%s' % profile), freeze_dir) \ No newline at end of file diff --git a/fbs/freeze/linux.py b/fbs/freeze/linux.py index 7affcaf..0742873 100644 --- a/fbs/freeze/linux.py +++ b/fbs/freeze/linux.py @@ -7,7 +7,7 @@ def freeze_linux(debug=False): run_pyinstaller(debug=debug) _generate_resources() - copy(path('src/main/icons/Icon.ico'), path('${freeze_dir}')) + copy(path('${icons_dir}/Icon.ico'), path('${freeze_dir}')) # For some reason, PyInstaller packages libstdc++.so.6 even though it is # available on most Linux distributions. If we include it and run our app on # a different Ubuntu version, then Popen(...) calls fail with errors diff --git a/fbs/freeze/windows.py b/fbs/freeze/windows.py index c7b898c..b8fe8c3 100644 --- a/fbs/freeze/windows.py +++ b/fbs/freeze/windows.py @@ -16,14 +16,14 @@ def freeze_windows(debug=False): # The --windowed flag below prevents us from seeing any console output. # We therefore only add it when we're not debugging. args.append('--windowed') - args.extend(['--icon', path('src/main/icons/Icon.ico')]) + args.extend(['--icon', path('${icons_dir}/Icon.ico')]) for path_fn in default_path, path: - _copy(path_fn, 'src/freeze/windows/version_info.py', path('target/PyInstaller')) + _copy(path_fn, '%s/windows/version_info.py' % SETTINGS['freeze_config_dir'], path('target/PyInstaller')) args.extend(['--version-file', path('target/PyInstaller/version_info.py')]) run_pyinstaller(args, debug) _restore_corrupted_python_dlls() _generate_resources() - copy(path('src/main/icons/Icon.ico'), path('${freeze_dir}')) + copy(path('${icons_dir}/Icon.ico'), path('${freeze_dir}')) _add_missing_dlls() def _restore_corrupted_python_dlls(): diff --git a/fbs/resources.py b/fbs/resources.py index acc9b17..dd31d5c 100644 --- a/fbs/resources.py +++ b/fbs/resources.py @@ -40,7 +40,7 @@ def get_icons(): """ result = {} for profile in LOADED_PROFILES: - icons_dir = 'src/main/icons/' + profile + icons_dir = path('${icons_dir}/%s' % profile) for icon_path in glob(path(icons_dir + '/*.png')): name = splitext(basename(icon_path))[0] match = re.match('(\d+)(?:@(\d+)x)?', name) diff --git a/fbs/sign/windows.py b/fbs/sign/windows.py index 7e62429..9bfa459 100644 --- a/fbs/sign/windows.py +++ b/fbs/sign/windows.py @@ -21,7 +21,7 @@ def sign_windows(): if 'windows_sign_pass' not in SETTINGS: raise FbsError( "Please set 'windows_sign_pass' to the password of %s in either " - "src/build/settings/secret.json, .../windows.json or .../base.json." + "%s/secret.json, .../windows.json or .../base.json." % SETTINGS['settings_dir'] % _CERTIFICATE_PATH ) for subdir, _, files in os.walk(path('${freeze_dir}')): diff --git a/fbs_runtime/_settings.py b/fbs_runtime/_settings.py index de60cac..cb15139 100644 --- a/fbs_runtime/_settings.py +++ b/fbs_runtime/_settings.py @@ -31,16 +31,23 @@ def load_settings(json_paths, base=None): for json_path in json_paths: with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) - result = data if result is None else _merge(result, data) + result = data if result is None else merge_settings(result, data) + + return expland_all_placeholders(result) + +def expland_all_placeholders(settings): + if settings is None: + return {} + while True: - for key, value in result.items(): - new_value = expand_placeholders(value, result) + for key, value in settings.items(): + new_value = expand_placeholders(value, settings) if new_value != value: - result[key] = new_value + settings[key] = new_value break else: break - return result + return settings def expand_placeholders(obj, settings): if isinstance(obj, str): @@ -52,7 +59,7 @@ def expand_placeholders(obj, settings): return {k: expand_placeholders(v, settings) for k, v in obj.items()} return obj -def _merge(a, b): +def merge_settings(a, b): if type(a) != type(b): raise ValueError('Cannot merge %r and %r' % (a, b)) if isinstance(a, list): @@ -60,6 +67,6 @@ def _merge(a, b): if isinstance(a, dict): result = dict(a) for k, v in b.items(): - result[k] = _merge(a[k], v) if k in a else v - return result + result[k] = merge_settings(a[k], v) if k in a else v + return expland_all_placeholders(result) return b \ No newline at end of file diff --git a/fbs_runtime/_source.py b/fbs_runtime/_source.py index 1356b76..a825bc1 100644 --- a/fbs_runtime/_source.py +++ b/fbs_runtime/_source.py @@ -6,7 +6,7 @@ from fbs_runtime import FbsError from fbs_runtime._fbs import get_default_profiles, get_core_settings, \ filter_public_settings -from fbs_runtime._settings import load_settings +from fbs_runtime._settings import load_settings, merge_settings from os.path import join, normpath, dirname, pardir, exists from pathlib import Path @@ -15,17 +15,17 @@ def get_project_dir(): result = Path(os.getcwd()) while result != result.parent: - if (result / 'src' / 'main' / 'python').is_dir(): + if (result / 'src' / 'build' / 'settings').is_dir() or Path('fbs_directories.json').is_file(): return str(result) result = result.parent raise FbsError( 'Could not determine the project base directory. ' - 'Was expecting src/main/python.' + 'Was expecting src/build/settings or fbs_directories.json.' ) -def get_resource_dirs(project_dir): - result = [path(project_dir, 'src/main/icons')] - resources = path(project_dir, 'src/main/resources') +def get_resource_dirs(project_dir, settings): + result = [path(project_dir, settings['icons_dir'])] + resources = path(project_dir, settings['resources_dir']) result.extend( join(resources, profile) # Resource dirs are listed most-specific first whereas profiles are @@ -37,17 +37,36 @@ def get_resource_dirs(project_dir): def load_build_settings(project_dir): core_settings = get_core_settings(project_dir) profiles = get_default_profiles() - json_paths = get_settings_paths(project_dir, profiles) - all_settings = load_settings(json_paths, core_settings) + + all_settings = load_settings_from_paths(project_dir, profiles, core_settings) + return filter_public_settings(all_settings) -def get_settings_paths(project_dir, profiles): - return list(filter(exists, ( - path_fn('src/build/settings/%s.json' % profile) - for path_fn in (default_path, lambda p: path(project_dir, p)) +def load_settings_from_paths(project_dir, profiles, core_settings): + initial_settings_paths = get_settings_paths(lambda p: default_path("src/build/settings/%s" % p), profiles) + + path_settings_file = path(project_dir, "fbs_directories.json") + if exists(path_settings_file): + initial_settings_paths.append(path_settings_file) + + initial_settings = load_settings(initial_settings_paths, core_settings) + + # extract project settings paths + user_settings_dirs = get_settings_paths( + lambda p: path(project_dir, "%s/%s" % (initial_settings['settings_dir'], p)), profiles) + + user_settings = load_settings(user_settings_dirs) + + return merge_settings(initial_settings, user_settings) + +def get_settings_paths(path_fn, profiles): + build_settings_paths = list(filter(exists, ( + path_fn('%s.json' % profile) for profile in profiles ))) + return build_settings_paths + def default_path(path_str): defaults_dir = join(dirname(__file__), pardir, 'fbs', '_defaults') return path(defaults_dir, path_str) diff --git a/fbs_runtime/application_context/__init__.py b/fbs_runtime/application_context/__init__.py index f255998..4e2af36 100644 --- a/fbs_runtime/application_context/__init__.py +++ b/fbs_runtime/application_context/__init__.py @@ -119,7 +119,7 @@ def _resource_locator(self): if is_frozen(): resource_dirs = _frozen.get_resource_dirs() else: - resource_dirs = _source.get_resource_dirs(self._project_dir) + resource_dirs = _source.get_resource_dirs(self._project_dir, self.build_settings) return ResourceLocator(resource_dirs) @cached_property def _project_dir(self):