diff --git a/tvb_build/app.entitlements b/tvb_build/app.entitlements
new file mode 100644
index 0000000000..de5bf5a3c7
--- /dev/null
+++ b/tvb_build/app.entitlements
@@ -0,0 +1,18 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.network.client
+
+ com.apple.security.network.server
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+ com.apple.security.cs.allow-dyld-environment-variables
+
+
+
diff --git a/tvb_build/app.inner.entitlements b/tvb_build/app.inner.entitlements
new file mode 100644
index 0000000000..7e86413d0e
--- /dev/null
+++ b/tvb_build/app.inner.entitlements
@@ -0,0 +1,16 @@
+
+
+
+
+ com.apple.security.network.client
+
+ com.apple.security.network.server
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+ com.apple.security.cs.allow-dyld-environment-variables
+
+
+
diff --git a/tvb_build/conda_env_to_app.py b/tvb_build/conda_env_to_app.py
index 3a351fdaa9..4375f845b7 100644
--- a/tvb_build/conda_env_to_app.py
+++ b/tvb_build/conda_env_to_app.py
@@ -25,6 +25,7 @@
#
"""
+.. moduleauthor:: Lia Domide
.. moduleauthor:: Bogdan Valean
"""
@@ -56,8 +57,11 @@
VERSION = TvbProfile.current.version.BASE_VERSION
# Name of the app
APP_NAME = "tvb-{}".format(VERSION)
-# The website in reversered order (domain first, etc.)
-IDENTIFIER = "org.thevirtualbrain"
+# should match an Apple Developer defined identifier
+IDENTIFIER = "ro.codemart.tvb"
+# KEYs for the ENV variable where we expect the signing identity to be defined
+KEY_SIGN_IDENTITY = "SIGN_APP_IDENTITY"
+KEY_MAC_PWD = "MAC_PASSWORD"
# The author of this package
AUTHOR = "TVB Team"
# Full path to the anaconda environment folder to package
@@ -90,6 +94,8 @@
# Path to the icon of the app
ICON_PATH = os.path.join(TVB_ROOT, "tvb_build", "icon.icns")
+# Absolute path towards TVB license file, to be included in the .app
+LICENSE_PATH = os.path.join(TVB_ROOT, "LICENSE")
# The entry script of the application in the environment's bin folder
ENTRY_SCRIPT = "-m tvb_bin.app"
# Folder to place created APP and DMG in.
@@ -156,8 +162,15 @@
DMG_ICON_SIZE = 80
+def _log(msg, indent=1):
+ if indent == 1:
+ print(" - ", msg)
+ else:
+ print(" " * indent, msg)
+
+
def extra():
- fix_paths()
+ _fix_paths()
def _find_and_replace(path, search, replace, exclusions=None):
@@ -207,9 +220,9 @@ def _find_and_replace(path, search, replace, exclusions=None):
stream.nextfile()
-def replace_conda_abs_paths():
+def _replace_conda_abs_paths():
app_path = os.path.join(os.path.sep, 'Applications', APP_NAME + '.app', 'Contents', 'Resources')
- print('Replacing occurences of {} with {}'.format(CONDA_ENV_PATH, app_path))
+ _log('Replacing occurences of {} with {}'.format(CONDA_ENV_PATH, app_path), 2)
_find_and_replace(
RESOURCE_DIR,
CONDA_ENV_PATH,
@@ -219,42 +232,42 @@ def replace_conda_abs_paths():
def create_app():
- print("Output Dir {}".format(OUTPUT_FOLDER))
""" Create an app bundle """
+ _log("Output Dir {}".format(OUTPUT_FOLDER), 2)
if os.path.exists(APP_FILE):
shutil.rmtree(APP_FILE)
- print("\n++++++++++++++++++++++++ Creating APP +++++++++++++++++++++++++++")
+ _log("Creating APP ", 1)
start_t = time.time()
- create_app_structure()
- copy_anaconda_env()
+ _create_app_structure()
+ _copy_anaconda_env()
if ICON_FILE:
- copy_icon()
- create_plist()
+ _copy_icon_and_license()
+ _create_plist()
# Do some package specific stuff, which is defined in the extra() function
# in settings.py (and was imported at the top of this module)
if "extra" in globals() and callable(extra):
- print("Performing application specific actions.")
+ _log("Performing application specific actions.", 2)
extra()
- replace_conda_abs_paths()
+ _replace_conda_abs_paths()
- print("============ APP CREATION FINISHED in {} seconds ====================".format(int(time.time() - start_t)))
+ _log("APP creation finished in {} seconds".format(int(time.time() - start_t)), 2)
-def create_app_structure():
+def _create_app_structure():
""" Create folder structure comprising a Mac app """
- print("Creating app structure")
+ _log("Creating app structure", 2)
try:
os.makedirs(MACOS_DIR)
except OSError as e:
- print('Could not create app structure: {}'.format(e))
+ _log('!!!Could not create app structure: {}'.format(e))
sys.exit(1)
- print("Creating app entry script")
+ _log("Creating app entry script", 2)
with open(APP_SCRIPT, 'w') as fp:
# Write the contents
try:
@@ -262,7 +275,7 @@ def create_app_structure():
"script_dir=$(dirname \"$(dirname \"$0\")\")\n"
"$script_dir/Resources/bin/python "
"{} $@".format(ENTRY_SCRIPT))
- except IOError as e:
+ except IOError:
logger.exception("Could not create Contents/OpenSesame script")
sys.exit(1)
@@ -272,9 +285,9 @@ def create_app_structure():
stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
-def copy_anaconda_env():
+def _copy_anaconda_env():
""" Copy anaconda environment """
- print("Copying Anaconda environment (this may take a while)")
+ _log("Copying Anaconda environment (this may take a while)", 2)
try:
if "CONDA_FOLDERS" in globals():
# IF conda folders is specified, copy only those folders.
@@ -306,22 +319,32 @@ def copy_anaconda_env():
os.remove(item)
else:
logger.warning("File not found: {}".format(item))
- except (IOError, OSError) as e:
+ except (IOError, OSError):
logger.error("WARNING: could not delete {}".format(item))
-def copy_icon():
+def _copy_icon_and_license():
""" Copy icon to Resources folder """
global ICON_PATH
- print("Copying icon file")
+ _log("Copying icon file", 2)
try:
shutil.copy(ICON_PATH, os.path.join(RESOURCE_DIR, ICON_FILE))
- except OSError as e:
- logger("Error copying icon file from: {}".format(ICON_PATH))
+ except OSError:
+ logger.error("Error copying icon file from: {}".format(ICON_PATH))
+ global LICENSE_PATH
+ _log("Copying license file", 2)
+ try:
+ unnecessary_file = os.path.join(RESOURCE_DIR, "LICENSE.txt")
+ if os.path.exists(unnecessary_file):
+ os.remove(unnecessary_file)
+ shutil.copy(LICENSE_PATH, RESOURCE_DIR)
+ except OSError:
+ logger.error("Error copying license file from: {}".format(LICENSE_PATH))
-def create_plist():
- print("Creating Info.plist")
+
+def _create_plist():
+ _log("Creating Info.plist", 2)
global ICON_FILE
global VERSION
@@ -341,7 +364,7 @@ def create_plist():
'CFBundlePackageType': 'APPL',
'CFBundleVersion': LONG_VERSION,
'CFBundleShortVersionString': VERSION,
- 'CFBundleSignature': '????',
+ 'CFBundleSignature': '????', # ok not to be setup
'LSMinimumSystemVersion': '10.7.0',
'LSUIElement': False,
'NSAppTransportSecurity': {'NSAllowsArbitraryLoads': True},
@@ -366,6 +389,86 @@ def create_plist():
plistlib.dump(info_plist_data, fp)
+excluded_parts = [".dist-info", "egg-info", "ignore", "COPYING", "Makefile", "README", "LICENSE",
+ "draft", ".prettierrc", "zoneinfo/", "_vendored"]
+
+
+def _should_be_signed(current_path):
+ if os.path.islink(current_path) or os.path.isdir(current_path):
+ return False
+ file_ext = os.path.splitext(current_path)[1]
+ if file_ext in (".dylib", ".so"):
+ return True
+ if file_ext in ("", ".10", ".6", ".local"):
+ for excl in excluded_parts:
+ if excl in current_path:
+ return False
+ return os.system("file -b " + current_path + " | grep text > /dev/null")
+ return False
+
+
+def _codesign_inside(root_path, command_prefix, dev_identity, ent_file):
+ # _log(f"Signing in folder {root_path}", 2)
+ for path_sufix in os.listdir(root_path):
+ current_path = os.path.join(root_path, path_sufix)
+ if _should_be_signed(current_path):
+ # _log(f"Signing {current_path}", 2)
+ os.system(f"{command_prefix} codesign -s '{dev_identity}' -o runtime -f "
+ f"--timestamp --entitlements '{ent_file}' '{current_path}'")
+ if os.path.isdir(current_path) and not os.path.islink(current_path):
+ _codesign_inside(current_path, command_prefix, dev_identity, ent_file)
+
+
+def sign_app(app_path=APP_FILE, app_zip_path=os.path.join(OUTPUT_FOLDER, "tvb.zip"),
+ ent_file=os.path.join(TVB_ROOT, "tvb_build", "app.entitlements")):
+ """
+ Sign a .APP file, with an Apple Developer Identity previously installed on the current machine.
+ The identity can be found through command "security find-identity".
+
+ We expect these as ENV variables of Jenskins build machine:
+ - SIGN_APP_IDENTITY - to be found with `security find-identity` command
+ - MAC_PASSWORD
+ """
+ if KEY_SIGN_IDENTITY not in os.environ or KEY_MAC_PWD not in os.environ:
+ _log(f"!! We can not sign the resulting .app because the {KEY_SIGN_IDENTITY} and "
+ f"{KEY_MAC_PWD} variables are not in ENV!!")
+ return
+
+ dev_identity = os.environ.get(KEY_SIGN_IDENTITY)
+ mac_pwd = os.environ.get(KEY_MAC_PWD)
+ _log(f"Preparing to sign: {app_path} with {dev_identity}")
+
+ os.system(f"security find-identity") # for debug purposes only, to find the current installed keys on this machine
+
+ # When executing signing over SSH (like Jenkins does), we first need to unclock the keychain
+ prefix = f"security unlock-keychain -p {mac_pwd} /Users/tvb/Library/Keychains/login.keychain &&"
+ # prefix = ""
+ # For inside binary files we need different entitlement set
+ inner_ent = os.path.join(TVB_ROOT, "tvb_build", "app.inner.entitlements")
+ _codesign_inside(os.path.join(app_path, "Contents", "Resources", "bin"), prefix, dev_identity, inner_ent)
+ _codesign_inside(os.path.join(app_path, "Contents", "Resources", "sbin"), prefix, dev_identity, inner_ent)
+ _codesign_inside(os.path.join(app_path, "Contents", "Resources", "lib"), prefix, dev_identity, inner_ent)
+ _log(f"Signing the main APP {app_path} with {ent_file}", 2)
+ os.system(f"{prefix} codesign -s '{dev_identity}' -f --timestamp -o runtime --entitlements '{ent_file}' '{app_path}'")
+ # Check the signing results
+ os.system(f"spctl -a -t exec -vv '{app_path}'")
+ os.system(f"codesign --verify --verbose=4 '{app_path}'")
+
+ _log(f"Compressing the main APP {app_path} into {app_zip_path}", 2)
+ os.system(f"/usr/bin/ditto -c -k --keepParent '{app_path}' '{app_zip_path}'")
+
+ # Storing credential has to me done once on the build machine before we can submit for notarization:
+ # xcrun notarytool store-credentials --apple-id {env.SIGN_APPLE_ID} --password {env.SIGN_APP_PASSWORD} --team-id {env.SIGN_TEAM_ID} --verbose --keychain-profile "tvb"
+ _log(f"Submitting for notarization {app_zip_path} ...")
+ os.system(f"{prefix} xcrun notarytool submit '{app_zip_path}' --keychain-profile 'tvb' "
+ f"--wait --webhook 'https://example.com/notarization'")
+ # xcrun notarytool log --keychain-profile "tvb" {ID from submit command: 72c04616-8f6a-401d-94f5-c20d47e35138} errors.txt
+ # Staple the notarization ticket and inspect status after
+ os.system(f"xcrun stapler staple '{app_path}'")
+ os.system(f"spctl -a -t exec -vv '{app_path}'")
+ os.remove(app_zip_path)
+
+
def create_dmg():
""" Create a dmg of the app """
@@ -379,7 +482,7 @@ def create_dmg():
if os.path.exists(dmg_file):
os.remove(dmg_file)
- print("\n+++++++++++++++++++++ Creating DMG from app +++++++++++++++++++++++")
+ _log("Creating DMG from app...")
# Get file size of APP
app_size = subprocess.check_output(
@@ -390,11 +493,10 @@ def create_dmg():
# Add a bit of extra to the disk image size
app_size = str(float(size) * 1.25) + unit
- print("Creating disk image of {}".format(app_size))
+ _log("Creating disk image of {}".format(app_size), 2)
# Create a dmgbuild config file in same folder as
- dmgbuild_config_file = os.path.join(os.getcwd(),
- 'dmgbuild_settings.py')
+ dmgbuild_config_file = os.path.join(os.getcwd(), 'dmgbuild_settings.py')
dmg_config = {
'filename': dmg_file,
@@ -411,15 +513,14 @@ def create_dmg():
dmg_config['icon_locations'] = DMG_ICON_LOCATIONS
dmg_config['window_rect'] = DMG_WINDOW_RECT
- write_vars_to_file(dmgbuild_config_file, dmg_config)
- print("Copying files to DMG and compressing it. Please wait.")
+ _write_vars_to_file(dmgbuild_config_file, dmg_config)
+ _log("Copying files to DMG and compressing it. Please wait...", 2)
dmgbuild.build_dmg(dmg_file, APP_NAME, settings_file=dmgbuild_config_file)
-
- # Clean up!
+ _log("Clean up!", 2)
os.remove(dmgbuild_config_file)
-def write_vars_to_file(file_path, var_dict):
+def _write_vars_to_file(file_path, var_dict):
with open(file_path, 'w') as fp:
fp.write("# -*- coding: utf-8 -*-\n")
fp.write("from __future__ import unicode_literals\n\n")
@@ -431,18 +532,18 @@ def write_vars_to_file(file_path, var_dict):
fp.write('{} = {}\n'.format(var, value))
-def fix_paths():
- kernel_json = os.path.join(
- RESOURCE_DIR, 'share', 'jupyter', 'kernels', 'python3', 'kernel.json')
+def _fix_paths():
+ kernel_json = os.path.join(RESOURCE_DIR, 'share', 'jupyter', 'kernels', 'python3', 'kernel.json')
if os.path.exists(kernel_json):
- print('Fixing kernel.json')
+ _log('Fixing kernel.json', 2)
with open(kernel_json, 'r') as fp:
- kernelCfg = json.load(fp)
- kernelCfg['argv'][0] = 'python'
+ kernel_cfg = json.load(fp)
+ kernel_cfg['argv'][0] = 'python'
with open(kernel_json, 'w+') as fp:
- json.dump(kernelCfg, fp)
+ json.dump(kernel_cfg, fp)
if __name__ == "__main__":
create_app()
+ sign_app()
create_dmg()
diff --git a/tvb_build/setup_mac.py b/tvb_build/setup_mac.py
index b41e326725..d90971483c 100644
--- a/tvb_build/setup_mac.py
+++ b/tvb_build/setup_mac.py
@@ -40,7 +40,7 @@
import tvb_bin
from glob import glob
from zipfile import ZipFile, ZIP_DEFLATED
-from conda_env_to_app import create_app, create_dmg, APP_NAME
+from conda_env_to_app import create_app, create_dmg, APP_NAME, sign_app
from tvb.basic.profile import TvbProfile
from tvb.basic.config.environment import Environment
from tvb_build.third_party_licenses.build_licenses import generate_artefact
@@ -199,6 +199,7 @@ def _generate_distribution(final_name, library_path, version, extra_licensing_ch
online_help_dst = os.path.join(library_abs_path, "tvb", "interfaces", "web", "static", "help")
print("- Moving " + online_help_src + " to " + online_help_dst)
os.rename(online_help_src, online_help_dst)
+ sign_app()
create_dmg()
print("- Cleaning up non-required files...")
diff --git a/tvb_library/tvb/basic/config/environment.py b/tvb_library/tvb/basic/config/environment.py
index 376a44e3b6..6ba2b5e803 100644
--- a/tvb_library/tvb/basic/config/environment.py
+++ b/tvb_library/tvb/basic/config/environment.py
@@ -72,10 +72,9 @@ def is_distribution():
pass
try:
- import tvb
- externals_path = os.path.join(
- os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(tvb.__file__)))),
- "dev_resources")
+ externals_path = os.path.join(os.path.abspath(os.path.join(__file__, os.path.pardir, os.path.pardir,
+ os.path.pardir, os.path.pardir, os.path.pardir)),
+ "dev_resources")
if os.path.exists(externals_path):
# usage from GitHub clone without got cmd or inside a Docker container (as a mounted volume)
return False
diff --git a/tvb_library/tvb/basic/config/settings.py b/tvb_library/tvb/basic/config/settings.py
index 20e3a8d527..6b89f4482c 100644
--- a/tvb_library/tvb/basic/config/settings.py
+++ b/tvb_library/tvb/basic/config/settings.py
@@ -233,7 +233,7 @@ def __init__(self, manager):
self.ENCRYPT_STORAGE = manager.get_attribute(stored.KEY_ENCRYPT_STORAGE, False, eval)
self.DECRYPT_PATH = manager.get_attribute(stored.KEY_DECRYPT_PATH)
- self.CURRENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+ self.CURRENT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
try:
import tvb.interfaces
diff --git a/tvb_library/tvb/basic/profile.py b/tvb_library/tvb/basic/profile.py
index 28ed84f208..5fbd84bde3 100644
--- a/tvb_library/tvb/basic/profile.py
+++ b/tvb_library/tvb/basic/profile.py
@@ -99,7 +99,7 @@ def _build_profile_class(cls, selected_profile, in_operation=False, run_init=Tru
else:
msg = "Invalid profile name %r, expected one of %r"
- msg %= (selected_profile, cls.ALL)
+ msg %= (selected_profile, cls.REGISTERED_PROFILES.keys())
raise Exception(msg)
@classmethod