1#!/usr/local/bin/python3.8 2 3# 4# This source file is part of appleseed. 5# Visit https://appleseedhq.net/ for additional information and resources. 6# 7# This software is released under the MIT license. 8# 9# Copyright (c) 2010-2013 Francois Beaune, Jupiter Jazz Limited 10# Copyright (c) 2014-2018 Francois Beaune, The appleseedhq Organization 11# 12# Permission is hereby granted, free of charge, to any person obtaining a copy 13# of this software and associated documentation files (the "Software"), to deal 14# in the Software without restriction, including without limitation the rights 15# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16# copies of the Software, and to permit persons to whom the Software is 17# furnished to do so, subject to the following conditions: 18# 19# The above copyright notice and this permission notice shall be included in 20# all copies or substantial portions of the Software. 21# 22# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28# THE SOFTWARE. 29# 30 31from __future__ import print_function 32from distutils import archive_util, dir_util 33from xml.etree.ElementTree import ElementTree 34import argparse 35import fnmatch 36import glob 37import os 38import platform 39import re 40import shutil 41import stat 42import subprocess 43import sys 44import time 45import traceback 46import zipfile 47 48 49# ------------------------------------------------------------------------------------------------- 50# Constants. 51# ------------------------------------------------------------------------------------------------- 52 53VERSION = "2.6.0" 54SETTINGS_FILENAME = "appleseed.package.configuration.xml" 55 56 57# ------------------------------------------------------------------------------------------------- 58# Utility functions. 59# ------------------------------------------------------------------------------------------------- 60 61def info(message): 62 print(" {0}".format(message)) 63 64 65def progress(message): 66 print(" {0}...".format(message)) 67 68 69def fatal(message): 70 print("Fatal: {0}. Aborting.".format(message)) 71 if sys.exc_info()[0]: 72 print(traceback.format_exc()) 73 sys.exit(1) 74 75 76def exe(filepath): 77 return filepath + ".exe" if os.name == "nt" else filepath 78 79 80def safe_delete_file(path): 81 try: 82 if os.path.exists(path): 83 os.remove(path) 84 except OSError: 85 fatal("Failed to delete file {0}".format(path)) 86 87 88def on_rmtree_error(func, path, exc_info): 89 # path contains the path of the file that couldn't be removed. 90 # Let's just assume that it's read-only and unlink it. 91 os.chmod(path, stat.S_IWRITE) 92 os.unlink(path) 93 94 95def safe_delete_directory(path): 96 Attempts = 10 97 for attempt in range(Attempts): 98 try: 99 if os.path.exists(path): 100 shutil.rmtree(path, onerror=on_rmtree_error) 101 return 102 except OSError: 103 if attempt < Attempts - 1: 104 time.sleep(0.5) 105 else: 106 fatal("Failed to delete directory {0}".format(path)) 107 108 109def safe_make_directory(path): 110 if not os.path.isdir(path): 111 os.makedirs(path) 112 113 114def pushd(path): 115 old_path = os.getcwd() 116 os.chdir(path) 117 return old_path 118 119 120def extract_zip_file(zip_path, output_path): 121 zf = zipfile.ZipFile(zip_path) 122 zf.extractall(output_path) 123 zf.close() 124 125 126def copy_glob(input_pattern, output_path): 127 for input_file in glob.glob(input_pattern): 128 shutil.copy(input_file, output_path) 129 130 131def make_writable(filepath): 132 os.chmod(filepath, stat.S_IRUSR | stat.S_IWUSR) 133 134 135def merge_tree(src, dst, symlinks=False, ignore=None): 136 names = os.listdir(src) 137 if ignore is not None: 138 ignored_names = ignore(src, names) 139 else: 140 ignored_names = set() 141 142 if not os.path.exists(dst): 143 os.makedirs(dst) 144 145 errors = [] 146 for name in names: 147 if name in ignored_names: 148 continue 149 srcname = os.path.join(src, name) 150 dstname = os.path.join(dst, name) 151 try: 152 if symlinks and os.path.islink(srcname): 153 linkto = os.readlink(srcname) 154 os.symlink(linkto, dstname) 155 elif os.path.isdir(srcname): 156 merge_tree(srcname, dstname, symlinks, ignore) 157 else: 158 # Will raise a SpecialFileError for unsupported file types. 159 shutil.copy2(srcname, dstname) 160 # Catch the Error from the recursive copytree so that we can 161 # continue with other files. 162 except Error as ex: 163 errors.extend(ex.args[0]) 164 except EnvironmentError as ex: 165 errors.append((srcname, dstname, str(ex))) 166 try: 167 shutil.copystat(src, dst) 168 except OSError as ex: 169 if WindowsError is not None and isinstance(ex, WindowsError): 170 # Copying file access times may fail on Windows. 171 pass 172 else: 173 errors.append((src, dst, str(ex))) 174 if errors: 175 raise Error(errors) 176 177 178# ------------------------------------------------------------------------------------------------- 179# Settings. 180# ------------------------------------------------------------------------------------------------- 181 182class Settings: 183 184 def load(self): 185 print("Loading settings from {0}...".format(SETTINGS_FILENAME)) 186 tree = ElementTree() 187 try: 188 tree.parse(SETTINGS_FILENAME) 189 except IOError: 190 fatal("Failed to load configuration file {0}".format(SETTINGS_FILENAME)) 191 self.__load_values(tree) 192 self.__print_summary() 193 194 def __load_values(self, tree): 195 self.platform = self.__get_required(tree, "platform") 196 self.configuration = self.__get_required(tree, "configuration") 197 self.appleseed_path = self.__get_required(tree, "appleseed_path") 198 self.appleseed_headers_path = self.__get_required(tree, "appleseed_headers_path") 199 self.qt_runtime_path = self.__get_required(tree, "qt_runtime_path") 200 self.platform_runtime_path = self.__get_required(tree, "platform_runtime_path") 201 self.python_path = self.__get_required(tree, "python_path") 202 self.package_output_path = self.__get_required(tree, "package_output_path") 203 204 def __get_required(self, tree, key): 205 value = tree.findtext(key) 206 if value is None: 207 fatal("Missing value \"{0}\" in configuration file".format(key)) 208 return value 209 210 def __print_summary(self): 211 print("") 212 print(" Platform: {0}".format(self.platform)) 213 print(" Configuration: {0}".format(self.configuration)) 214 print(" Path to appleseed: {0}".format(self.appleseed_path)) 215 print(" Path to appleseed headers: {0}".format(self.appleseed_headers_path)) 216 print(" Path to Qt runtime: {0}".format(self.qt_runtime_path)) 217 if os.name == "nt": 218 print(" Path to platform runtime: {0}".format(self.platform_runtime_path)) 219 print(" Path to Python 2.7: {0}".format(self.python_path)) 220 print(" Output directory: {0}".format(self.package_output_path)) 221 print("") 222 223 224# ------------------------------------------------------------------------------------------------- 225# Package information. 226# ------------------------------------------------------------------------------------------------- 227 228class PackageInfo: 229 230 def __init__(self, settings, no_zip): 231 self.no_zip = no_zip 232 self.settings = settings 233 234 def load(self): 235 print("Loading package information...") 236 self.retrieve_git_tag() 237 self.build_package_path() 238 self.print_summary() 239 240 def retrieve_git_tag(self): 241 old_path = pushd(self.settings.appleseed_path) 242 self.version = subprocess.check_output(["git", "describe", "--long"]).decode("utf-8").strip() 243 os.chdir(old_path) 244 245 def build_package_path(self): 246 package_dir = "appleseed-{0}".format(self.version) 247 package_name = "appleseed-{0}-{1}.zip".format(self.version, self.settings.platform) 248 self.package_path = os.path.join(self.settings.package_output_path, package_dir, package_name) 249 250 def print_summary(self): 251 print("") 252 print(" Version: {0}".format(self.version)) 253 if not self.no_zip: 254 print(" Package path: {0}".format(self.package_path)) 255 else: 256 print(" Package directory: {0}".format(self.settings.package_output_path)) 257 print("") 258 259 260# ------------------------------------------------------------------------------------------------- 261# Base package builder. 262# ------------------------------------------------------------------------------------------------- 263 264class PackageBuilder: 265 266 def __init__(self, settings, package_info): 267 self.settings = settings 268 self.package_info = package_info 269 270 def build_package(self): 271 print("Building package:") 272 print("") 273 self.orchestrate() 274 print("") 275 print("The package was successfully built.") 276 277 def orchestrate(self): 278 self.remove_leftovers() 279 self.retrieve_sandbox_from_git_repository() 280 self.deploy_sandbox_to_stage() 281 self.cleanup_stage() 282 self.add_local_binaries_to_stage() 283 self.add_local_libraries_to_stage() 284 self.add_headers_to_stage() 285 self.add_shaders_to_stage() 286 self.add_scripts_to_stage() 287 self.add_local_schema_files_to_stage() 288 self.add_text_files_to_stage() 289 self.add_dummy_files_into_empty_directories() 290 self.disable_system_qt_plugins() 291 self.alter_stage() 292 if self.package_info.no_zip: 293 self.deploy_stage_to_package_directory() 294 else: 295 self.build_final_zip_file() 296 self.remove_stage() 297 298 def remove_leftovers(self): 299 progress("Removing leftovers from previous invocations") 300 safe_delete_directory("appleseed") 301 safe_delete_file("sandbox.zip") 302 safe_delete_file(self.package_info.package_path) 303 304 def retrieve_sandbox_from_git_repository(self): 305 progress("Retrieving sandbox from Git repository") 306 old_path = pushd(os.path.join(self.settings.appleseed_path, "sandbox")) 307 self.run("git archive --format=zip --output={0} --worktree-attributes HEAD".format(os.path.join(old_path, "sandbox.zip"))) 308 os.chdir(old_path) 309 310 def deploy_sandbox_to_stage(self): 311 progress("Deploying sandbox to staging directory") 312 extract_zip_file("sandbox.zip", "appleseed/") 313 safe_delete_file("sandbox.zip") 314 315 def cleanup_stage(self): 316 progress("Cleaning up staging directory") 317 318 # Remove API reference documentation. 319 safe_delete_directory("appleseed/documentation/apireference") 320 321 # Remove the test suite. 322 safe_delete_directory("appleseed/tests/test scenes") 323 324 # Remove voluminous unit tests/benchmarks data. 325 safe_delete_file("appleseed/tests/unit benchmarks/inputs/test_knn_particles.bin") 326 safe_delete_file("appleseed/tests/unit benchmarks/inputs/test_knn_photons.bin") 327 328 # Temporarily remove Alembic assembly C++ plugin. 329 safe_delete_directory("appleseed/examples/cpp/alembicassembly") 330 331 def add_local_binaries_to_stage(self): 332 progress("Adding local binaries to staging directory") 333 safe_make_directory("appleseed/bin") 334 dir_util.copy_tree(os.path.join(self.settings.appleseed_path, "sandbox/bin", self.settings.configuration), "appleseed/bin/") 335 shutil.copy(os.path.join(self.settings.appleseed_path, "sandbox/bin", exe("maketx")), "appleseed/bin/") 336 shutil.copy(os.path.join(self.settings.appleseed_path, "sandbox/bin", exe("oiiotool")), "appleseed/bin/") 337 shutil.copy(os.path.join(self.settings.appleseed_path, "sandbox/bin", exe("idiff")), "appleseed/bin/") 338 shutil.copy(os.path.join(self.settings.appleseed_path, "sandbox/bin", exe("oslc")), "appleseed/bin/") 339 shutil.copy(os.path.join(self.settings.appleseed_path, "sandbox/bin", exe("oslinfo")), "appleseed/bin/") 340 341 def add_local_libraries_to_stage(self): 342 progress("Adding local libraries to staging directory") 343 safe_make_directory("appleseed/lib") 344 dir_util.copy_tree(os.path.join(self.settings.appleseed_path, "sandbox/lib", self.settings.configuration), "appleseed/lib/") 345 346 # 347 # This method is used by the Mac and Linux package builders. 348 # It requires the following members to be defined: 349 # 350 # self.shared_lib_ext 351 # self.get_dependencies_for_file() 352 # 353 354 def add_unix_dependencies_to_stage(self): 355 # Get shared libs needed by binaries. 356 bin_libs = set() 357 for dirpath, dirnames, filenames in os.walk("appleseed/bin"): 358 for filename in filenames: 359 ext = os.path.splitext(filename)[1] 360 if ext != ".py" and ext != ".conf": 361 libs = self.get_dependencies_for_file(os.path.join("appleseed/bin", filename)) 362 bin_libs = bin_libs.union(libs) 363 364 # Get shared libs needed by appleseed.python. 365 for dirpath, dirnames, filenames in os.walk("appleseed/lib"): 366 appleseedpython_shared_lib = "_appleseedpython" + self.shared_lib_ext 367 if appleseedpython_shared_lib in filenames: 368 libs = self.get_dependencies_for_file(os.path.join(dirpath, appleseedpython_shared_lib)) 369 bin_libs = bin_libs.union(libs) 370 371 # Get shared libs needed by libraries. 372 lib_libs = set() 373 for lib in bin_libs: 374 libs = self.get_dependencies_for_file(lib) 375 lib_libs = lib_libs.union(libs) 376 377 all_libs = bin_libs.union(lib_libs) 378 379 if False: 380 # Print dependencies. 381 info(" Dependencies:") 382 for lib in all_libs: 383 info(" {0}".format(lib)) 384 385 # Copy needed libs to lib directory. 386 dest_dir = os.path.join("appleseed", "lib/") 387 for lib in all_libs: 388 # The library might already exist, but without writing rights. 389 lib_name = os.path.basename(lib) 390 dest_path = os.path.join(dest_dir, lib_name) 391 if not os.path.exists(dest_path): 392 progress(" Copying {0} to {1}".format(lib, dest_dir)) 393 try: 394 shutil.copy(lib, dest_dir) 395 make_writable(dest_path) 396 except IOError: 397 info("WARNING: could not copy {0} to {1}".format(lib, dest_dir)) 398 399 def add_headers_to_stage(self): 400 progress("Adding headers to staging directory") 401 safe_make_directory("appleseed/include") 402 ignore_files = shutil.ignore_patterns("*.cpp", "*.c", "*.xsd", "snprintf", "version.h.in") 403 shutil.copytree(os.path.join(self.settings.appleseed_headers_path, "foundation"), "appleseed/include/foundation", ignore=ignore_files) 404 shutil.copytree(os.path.join(self.settings.appleseed_headers_path, "main"), "appleseed/include/main", ignore=ignore_files) 405 shutil.copytree(os.path.join(self.settings.appleseed_headers_path, "renderer"), "appleseed/include/renderer", ignore=ignore_files) 406 407 def add_shaders_to_stage(self): 408 progress("Adding shaders to staging directory") 409 safe_delete_directory("appleseed/shaders") 410 shutil.copytree(os.path.join(self.settings.appleseed_path, "sandbox/shaders"), "appleseed/shaders") 411 shutil.copytree(os.path.join(self.settings.appleseed_path, "src/appleseed.shaders/src"), "appleseed/shaders/src") 412 413 def add_scripts_to_stage(self): 414 progress("Adding scripts to staging directory") 415 shutil.copy("cleanmany.py", "appleseed/bin/") 416 shutil.copy("convertmany.py", "appleseed/bin/") 417 shutil.copy("rendermany.py", "appleseed/bin/") 418 shutil.copy("rendernode.py", "appleseed/bin/") 419 shutil.copy("rendermanager.py", "appleseed/bin/") 420 421 def add_local_schema_files_to_stage(self): 422 progress("Adding local schema files to staging directory") 423 safe_make_directory("appleseed/schemas") 424 copy_glob(os.path.join(self.settings.appleseed_path, "sandbox/schemas/*.xsd"), "appleseed/schemas/") 425 426 def add_text_files_to_stage(self): 427 progress("Adding text files") 428 shutil.copy(os.path.join(self.settings.appleseed_path, "LICENSE.txt"), "appleseed/") 429 shutil.copy(os.path.join(self.settings.appleseed_path, "README.md"), "appleseed/") 430 shutil.copy(os.path.join(self.settings.appleseed_path, "THIRDPARTIES.txt"), "appleseed/") 431 432 def add_dummy_files_into_empty_directories(self): 433 progress("Adding dummy files to preserve empty directories") 434 for dirpath, dirnames, filenames in os.walk("."): 435 if len(dirnames) == 0 and len(filenames) == 0: 436 self.create_preserve_file(dirpath) 437 438 def disable_system_qt_plugins(self): 439 progress("Disabling system's Qt plugins") 440 with open("appleseed/bin/qt.conf", "w") as f: 441 pass 442 443 def create_preserve_file(self, path): 444 with open(os.path.join(path, "preserve.txt"), "w") as f: 445 f.write("This file allows to preserve this otherwise empty directory.\n") 446 447 # This method is overridden in the platform-specific builders below. 448 def alter_stage(self): 449 return 450 451 def deploy_stage_to_package_directory(self): 452 package_directory = os.path.join(self.settings.package_output_path, "appleseed") 453 progress("Removing existing package directory") 454 safe_delete_directory(package_directory) 455 progress("Deploying staging directory to package directory") 456 shutil.copytree("appleseed", package_directory) 457 458 def build_final_zip_file(self): 459 progress("Building final zip file from staging directory") 460 package_base_path = os.path.splitext(self.package_info.package_path)[0] 461 archive_util.make_zipfile(package_base_path, "appleseed") 462 463 def remove_stage(self): 464 progress("Deleting staging directory") 465 safe_delete_directory("appleseed") 466 467 def run(self, cmdline): 468 info("Running command line: {0}".format(cmdline)) 469 os.system(cmdline) 470 471 def run_subprocess(self, cmdline): 472 p = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 473 out, err = p.communicate() 474 return p.returncode, out, err 475 476 477# ------------------------------------------------------------------------------------------------- 478# Windows package builder. 479# ------------------------------------------------------------------------------------------------- 480 481class WindowsPackageBuilder(PackageBuilder): 482 483 def alter_stage(self): 484 self.add_dependencies_to_stage() 485 self.add_python_to_stage() 486 487 def add_dependencies_to_stage(self): 488 progress("Windows-specific: Adding dependencies to staging directory") 489 self.copy_qt_framework("Qt5Core") 490 self.copy_qt_framework("Qt5Gui") 491 self.copy_qt_framework("Qt5OpenGL") 492 self.copy_qt_framework("Qt5Widgets") 493 copy_glob(os.path.join(self.settings.platform_runtime_path, "*"), "appleseed/bin/") 494 495 def add_python_to_stage(self): 496 progress("Windows-specific: Adding Python 2.7 to staging directory") 497 498 shutil.copy(os.path.join(self.settings.python_path, "Microsoft.VC90.CRT.manifest"), "appleseed/bin/") 499 shutil.copy(os.path.join(self.settings.python_path, "msvcr90.dll"), "appleseed/bin/") 500 shutil.copy(os.path.join(self.settings.python_path, "python.exe"), "appleseed/bin/") 501 shutil.copy(os.path.join(self.settings.python_path, "python27.dll"), "appleseed/bin/") 502 503 safe_make_directory("appleseed/python27") 504 shutil.copytree(os.path.join(self.settings.python_path, "DLLs"), "appleseed/python27/DLLs") 505 shutil.copytree(os.path.join(self.settings.python_path, "include"), "appleseed/python27/include") 506 shutil.copytree(os.path.join(self.settings.python_path, "Lib"), "appleseed/python27/Lib") 507 shutil.copytree(os.path.join(self.settings.python_path, "libs"), "appleseed/python27/libs") 508 shutil.copy(os.path.join(self.settings.python_path, "LICENSE.txt"), "appleseed/python27") 509 shutil.copy(os.path.join(self.settings.python_path, "README.txt"), "appleseed/python27") 510 511 def copy_qt_framework(self, framework_name): 512 src_filepath = os.path.join(self.settings.qt_runtime_path, framework_name + ".dll") 513 dst_path = os.path.join("appleseed", "bin") 514 shutil.copy(src_filepath, dst_path) 515 516 517# ------------------------------------------------------------------------------------------------- 518# Mac package builder. 519# ------------------------------------------------------------------------------------------------- 520 521class MacPackageBuilder(PackageBuilder): 522 523 def __init__(self, settings, package_info): 524 PackageBuilder.__init__(self, settings, package_info) 525 self.shared_lib_ext = ".dylib" 526 self.system_libs_prefixes = ["/System/Library/", "/usr/lib/libcurl", "/usr/lib/libc++", 527 "/usr/lib/libbz2", "/usr/lib/libSystem", "usr/lib/libz", 528 "/usr/lib/libncurses", "/usr/lib/libobjc.A.dylib"] 529 530 def alter_stage(self): 531 safe_delete_file("appleseed/bin/.DS_Store") 532 self.add_dependencies_to_stage() 533 self.add_python_to_stage() 534 self.fixup_binaries() 535 self.create_qt_conf_file() 536 os.rename("appleseed/bin/appleseed.studio", "appleseed/bin/appleseed-studio") 537 538 def add_dependencies_to_stage(self): 539 progress("Mac-specific: Adding dependencies to staging directory") 540 self.add_unix_dependencies_to_stage() 541 self.copy_qt_framework("QtCore") 542 self.copy_qt_framework("QtGui") 543 self.copy_qt_resources("QtGui") 544 self.copy_qt_framework("QtOpenGL") 545 546 def add_python_to_stage(self): 547 progress("Mac-specific: Adding Python 2.7 to staging directory") 548 safe_make_directory("appleseed/python27") 549 shutil.copytree(os.path.join(self.settings.python_path, "bin"), "appleseed/python27/bin") 550 shutil.copytree(os.path.join(self.settings.python_path, "include"), "appleseed/python27/include") 551 shutil.copytree(os.path.join(self.settings.python_path, "lib"), "appleseed/python27/lib") 552 shutil.copytree(os.path.join(self.settings.python_path, "share"), "appleseed/python27/share") 553 554 def copy_qt_framework(self, framework_name): 555 framework_dir = framework_name + ".framework" 556 src_filepath = os.path.join(self.settings.qt_runtime_path, framework_dir, "Versions", "4", framework_name) 557 dest_path = os.path.join("appleseed", "lib", framework_dir, "Versions", "4") 558 safe_make_directory(dest_path) 559 shutil.copy(src_filepath, dest_path) 560 make_writable(os.path.join(dest_path, framework_name)) 561 562 def copy_qt_resources(self, framework_name): 563 framework_dir = framework_name + ".framework" 564 src_path = os.path.join(self.settings.qt_runtime_path, framework_dir, "Versions", "4", "Resources") 565 dest_path = os.path.join("appleseed", "lib", framework_dir, "Resources") 566 shutil.copytree(src_path, dest_path) 567 568 def fixup_binaries(self): 569 progress("Mac-specific: Fixing up binaries") 570 self.set_libraries_ids() 571 self.set_qt_framework_ids() 572 self.change_library_paths_in_libraries() 573 self.change_library_paths_in_executables() 574 self.change_qt_framework_paths_in_qt_frameworks() 575 576 def set_libraries_ids(self): 577 for dirpath, dirnames, filenames in os.walk("appleseed/lib"): 578 for filename in filenames: 579 if os.path.splitext(filename)[1] == ".dylib": 580 lib_path = os.path.join(dirpath, filename) 581 self.set_library_id(lib_path, filename) 582 583 def set_qt_framework_ids(self): 584 self.set_library_id("appleseed/lib/QtCore.framework/Versions/4/QtCore", "QtCore.framework/Versions/4/QtCore") 585 self.set_library_id("appleseed/lib/QtGui.framework/Versions/4/QtGui", "QtGui.framework/Versions/4/QtGui") 586 self.set_library_id("appleseed/lib/QtOpenGL.framework/Versions/4/QtOpenGL", "QtOpenGL.framework/Versions/4/QtOpenGL") 587 588 def change_library_paths_in_libraries(self): 589 for dirpath, dirnames, filenames in os.walk("appleseed/lib"): 590 for filename in filenames: 591 ext = os.path.splitext(filename)[1] 592 if ext == ".dylib" or ext == ".so": 593 lib_path = os.path.join(dirpath, filename) 594 self.change_library_paths_in_binary(lib_path) 595 self.change_qt_framework_paths_in_binary(lib_path) 596 597 def change_library_paths_in_executables(self): 598 for dirpath, dirnames, filenames in os.walk("appleseed/bin"): 599 for filename in filenames: 600 ext = os.path.splitext(filename)[1] 601 if ext != ".py" and ext != ".conf": 602 exe_path = os.path.join(dirpath, filename) 603 self.change_library_paths_in_binary(exe_path) 604 self.change_qt_framework_paths_in_binary(exe_path) 605 606 # Can be used on executables and dynamic libraries. 607 def change_library_paths_in_binary(self, bin_path): 608 progress("Patching {0}".format(bin_path)) 609 bin_dir = os.path.dirname(bin_path) 610 path_to_appleseed_lib = os.path.relpath("appleseed/lib/", bin_dir) 611 for lib_path in self.get_dependencies_for_file(bin_path, fix_paths=False): 612 lib_name = os.path.basename(lib_path) 613 self.change_library_path(bin_path, lib_path, "@loader_path/{0}/{1}".format(path_to_appleseed_lib, lib_name)) 614 615 # Can be used on executables and dynamic libraries. 616 def change_qt_framework_paths_in_binary(self, bin_path): 617 for fwk_path in self.get_qt_frameworks_for_file(bin_path): 618 fwk_name = re.search(r"(Qt.*)\.framework", fwk_path).group(1) 619 self.change_library_path(bin_path, fwk_path, "@executable_path/../lib/{0}.framework/Versions/4/{0}".format(fwk_name)) 620 621 def change_qt_framework_paths_in_qt_frameworks(self): 622 self.change_qt_framework_paths_in_binary("appleseed/lib/QtCore.framework/Versions/4/QtCore") 623 self.change_qt_framework_paths_in_binary("appleseed/lib/QtGui.framework/Versions/4/QtGui") 624 self.change_qt_framework_paths_in_binary("appleseed/lib/QtOpenGL.framework/Versions/4/QtOpenGL") 625 626 def set_library_id(self, target, name): 627 self.run('install_name_tool -id "{0}" {1}'.format(name, target)) 628 629 def change_library_path(self, target, old, new): 630 self.run('install_name_tool -change "{0}" "{1}" {2}'.format(old, new, target)) 631 632 def get_dependencies_for_file(self, filename, fix_paths=True): 633 returncode, out, err = self.run_subprocess(["otool", "-L", filename]) 634 if returncode != 0: 635 fatal("Failed to invoke otool(1) to get dependencies for {0}: {1}".format(filename, err)) 636 637 libs = set() 638 639 for line in out.split("\n")[1:]: # skip the first line 640 line = line.strip() 641 642 # Ignore empty lines. 643 if len(line) == 0: 644 continue 645 646 # Parse the line. 647 m = re.match(r"(.*) \(compatibility version .*, current version .*\)", line) 648 if not m: 649 fatal("Failed to parse line from otool(1) output: {0}".format(line)) 650 lib = m.group(1) 651 652 # Ignore libs relative to @rpath. 653 if "@rpath" in lib: 654 continue 655 656 # Ignore libs relative to @loader_path. 657 if "@loader_path" in lib: 658 continue 659 660 # Ignore system libs. 661 if self.is_system_lib(lib): 662 continue 663 664 # Ignore Qt frameworks. 665 if re.search(r"Qt.*\.framework", lib): 666 continue 667 668 if fix_paths: 669 # Optionally search for libraries in other places. 670 if not os.path.exists(lib): 671 candidate = os.path.join("/usr/local/lib/", lib) 672 if os.path.exists(candidate): 673 lib = candidate 674 675 libs.add(lib) 676 677 if False: 678 info("Dependencies for file {0}:".format(filename)) 679 for lib in libs: 680 info(" {0}".format(lib)) 681 682 return libs 683 684 def get_qt_frameworks_for_file(self, filename, fix_paths=True): 685 returncode, out, err = self.run_subprocess(["otool", "-L", filename]) 686 if returncode != 0: 687 fatal("Failed to invoke otool(1) to get dependencies for {0}: {1}".format(filename, err)) 688 689 libs = set() 690 691 for line in out.split("\n")[1:]: # skip the first line 692 line = line.strip() 693 694 # Ignore empty lines. 695 if len(line) == 0: 696 continue 697 698 # Parse the line. 699 m = re.match(r"(.*) \(compatibility version .*, current version .*\)", line) 700 if not m: 701 fatal("Failed to parse line from otool(1) output: {0}".format(line)) 702 lib = m.group(1) 703 704 if re.search(r"Qt.*\.framework", lib): 705 libs.add(lib) 706 707 return libs 708 709 def is_system_lib(self, lib): 710 for prefix in self.system_libs_prefixes: 711 if lib.startswith(prefix): 712 return True 713 return False 714 715 def create_qt_conf_file(self): 716 safe_make_directory("appleseed/bin/Contents/Resources") 717 open("appleseed/bin/Contents/Resources/qt.conf", "w").close() 718 719 720# ------------------------------------------------------------------------------------------------- 721# Linux package builder. 722# ------------------------------------------------------------------------------------------------- 723 724class LinuxPackageBuilder(PackageBuilder): 725 726 def __init__(self, settings, package_info): 727 PackageBuilder.__init__(self, settings, package_info) 728 self.shared_lib_ext = ".so" 729 self.system_libs_prefixes = ["linux", "librt", "libpthread", "libGL", "libX", "libselinux", 730 "libICE", "libSM", "libdl", "libm.so", "libgcc", "libc.so", 731 "/lib64/ld-linux-", "libstdc++", "libxcb", "libdrm", "libnsl", 732 "libuuid", "libgthread", "libglib", "libgobject", "libglapi", 733 "libffi", "libfontconfig", "libutil", "libpython", 734 "libxshmfence.so"] 735 736 def alter_stage(self): 737 self.make_executable(os.path.join("appleseed/bin", "maketx")) 738 self.make_executable(os.path.join("appleseed/bin", "oiiotool")) 739 self.make_executable(os.path.join("appleseed/bin", "idiff")) 740 self.make_executable(os.path.join("appleseed/bin", "oslc")) 741 self.make_executable(os.path.join("appleseed/bin", "oslinfo")) 742 self.add_dependencies_to_stage() 743 self.set_runtime_paths_on_binaries() 744 self.clear_runtime_paths_on_libraries() 745 self.add_python_to_stage() # Must be last. 746 747 def make_executable(self, filepath): 748 mode = os.stat(filepath)[stat.ST_MODE] 749 mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH 750 os.chmod(filepath, mode) 751 752 def add_dependencies_to_stage(self): 753 progress("Linux-specific: Adding dependencies to staging directory") 754 self.add_unix_dependencies_to_stage() 755 756 def set_runtime_paths_on_binaries(self): 757 progress("Linux-specific: Setting runtime paths on binaries") 758 for dirpath, dirnames, filenames in os.walk("appleseed/bin"): 759 for filename in filenames: 760 ext = os.path.splitext(filename)[1] 761 if ext != ".py" and ext != ".conf": 762 self.run("chrpath -r \$ORIGIN/../lib {0}".format(os.path.join("appleseed/bin", filename))) 763 764 def clear_runtime_paths_on_libraries(self): 765 progress("Linux-specific: Clearing runtime paths on libraries") 766 for dirpath, dirnames, filenames in os.walk("appleseed/lib"): 767 for filename in filenames: 768 if os.path.splitext(filename)[1] == ".so": 769 self.run("chrpath -d {0}".format(os.path.join(dirpath, filename))) 770 771 def get_dependencies_for_file(self, filename): 772 returncode, out, err = self.run_subprocess(["ldd", filename]) 773 if returncode != 0: 774 fatal("Failed to invoke ldd(1) to get dependencies for {0}: {1}".format(filename, err)) 775 776 libs = set() 777 778 for line in out.split("\n"): 779 line = line.strip() 780 781 # Ignore empty lines. 782 if len(line) == 0: 783 continue 784 785 # Ignore system libs. 786 if self.is_system_lib(line): 787 continue 788 789 libs.add(line.split()[2]) 790 791 return libs 792 793 def is_system_lib(self, lib): 794 for prefix in self.system_libs_prefixes: 795 if lib.startswith(prefix): 796 return True 797 return False 798 799 def add_python_to_stage(self): 800 progress("Linux-specific: Adding Python 2.7 to staging directory") 801 802 merge_tree(os.path.join(self.settings.python_path, "bin"), "appleseed/bin", symlinks=True) 803 merge_tree(os.path.join(self.settings.python_path, "lib"), "appleseed/lib", symlinks=True) 804 merge_tree(os.path.join(self.settings.python_path, "include"), "appleseed/include", symlinks=True) 805 806 807# ------------------------------------------------------------------------------------------------- 808# Entry point. 809# ------------------------------------------------------------------------------------------------- 810 811def main(): 812 parser = argparse.ArgumentParser(description="build an appleseed package from sources") 813 814 parser.add_argument("--nozip", help="do not build a final zip file. Files will be copied to staging directory only", action="store_true") 815 816 args = parser.parse_args() 817 818 no_zip = args.nozip 819 820 print("appleseed.package version {0}".format(VERSION)) 821 print("") 822 823 print("IMPORTANT:") 824 print("") 825 print(" - You may need to run this tool with sudo on Linux and macOS") 826 print(" - Make sure there are no obsolete binaries in sandbox/bin and sandbox/lib") 827 print("") 828 829 settings = Settings() 830 package_info = PackageInfo(settings, no_zip) 831 832 settings.load() 833 package_info.load() 834 835 if os.name == "nt": 836 package_builder = WindowsPackageBuilder(settings, package_info) 837 elif os.name == "posix" and platform.mac_ver()[0] != "": 838 package_builder = MacPackageBuilder(settings, package_info) 839 elif os.name == "posix" and platform.mac_ver()[0] == "": 840 package_builder = LinuxPackageBuilder(settings, package_info) 841 else: 842 fatal("Unsupported platform: {0}".format(os.name)) 843 844 package_builder.build_package() 845 846 847if __name__ == "__main__": 848 main() 849