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