Skip to content

Commit

Permalink
Implemented custom directory structure
Browse files Browse the repository at this point in the history
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 mherrmann#268
  • Loading branch information
rainman110 committed Mar 7, 2022
1 parent 8ed25c1 commit 210e2cd
Show file tree
Hide file tree
Showing 11 changed files with 65 additions and 35 deletions.
7 changes: 3 additions & 4 deletions fbs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down
7 changes: 6 additions & 1 deletion fbs/_defaults/src/build/settings/base.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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/"],
Expand Down
2 changes: 1 addition & 1 deletion fbs/builtin_commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions fbs/freeze/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
_copy(path_fn, path('${resources_dir}/%s' % profile), resources_dest_dir)
_copy(path_fn, path('${freeze_config_dir}/%s' % profile), freeze_dir)
2 changes: 1 addition & 1 deletion fbs/freeze/linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions fbs/freeze/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
2 changes: 1 addition & 1 deletion fbs/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion fbs/sign/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}')):
Expand Down
23 changes: 15 additions & 8 deletions fbs_runtime/_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -52,14 +59,14 @@ 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):
return 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
43 changes: 31 additions & 12 deletions fbs_runtime/_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion fbs_runtime/application_context/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down

0 comments on commit 210e2cd

Please sign in to comment.