1#!/usr/bin/env python
2
3###################################
4# Author: Kevin Ollivier
5# Licence: wxWindows licence
6###################################
7
8import os
9import re
10import sys
11import builder
12import glob
13import optparse
14import platform
15import shutil
16import types
17import subprocess
18
19# builder object
20wxBuilder = None
21
22# other globals
23scriptDir = None
24wxRootDir = None
25contribDir = None
26options = None
27configure_opts = None
28exitWithException = True
29nmakeCommand = 'nmake.exe'
30
31verbose = False
32
33
34def numCPUs():
35    """
36    Detects the number of CPUs on a system.
37    This approach is from detectCPUs here: http://www.artima.com/weblogs/viewpost.jsp?thread=230001
38    """
39    # Linux, Unix and MacOS:
40    if hasattr(os, "sysconf"):
41        if "SC_NPROCESSORS_ONLN" in os.sysconf_names:
42            # Linux & Unix:
43            ncpus = os.sysconf("SC_NPROCESSORS_ONLN")
44            if isinstance(ncpus, int) and ncpus > 0:
45                return ncpus
46        else: # OSX:
47            p = subprocess.Popen("sysctl -n hw.ncpu", shell=True, stdout=subprocess.PIPE)
48            return p.stdout.read()
49
50    # Windows:
51    if "NUMBER_OF_PROCESSORS" in os.environ:
52            ncpus = int(os.environ["NUMBER_OF_PROCESSORS"]);
53            if ncpus > 0:
54                return ncpus
55    return 1 # Default
56
57
58def getXcodePaths():
59    base = getoutput("xcode-select -print-path")
60    return [base, base+"/Platforms/MacOSX.platform/Developer"]
61
62
63def getVisCVersion():
64    text = getoutput("cl.exe")
65    if 'Version 13' in text:
66        return '71'
67    if 'Version 15' in text:
68        return '90'
69    if 'Version 16' in text:
70        return '100'
71    # TODO: Add more tests to get the other versions...
72    else:
73        return 'FIXME'
74
75
76def exitIfError(code, msg):
77    if code != 0:
78        print(msg)
79        if exitWithException:
80            raise builder.BuildError(msg)
81        else:
82            sys.exit(1)
83
84
85def getWxRelease(wxRoot=None):
86    if not wxRoot:
87        global wxRootDir
88        wxRoot = wxRootDir
89
90    configureText = open(os.path.join(wxRoot, "configure.in"), "r").read()
91    majorVersion = re.search("wx_major_version_number=(\d+)", configureText).group(1)
92    minorVersion = re.search("wx_minor_version_number=(\d+)", configureText).group(1)
93
94    versionText = "%s.%s" % (majorVersion, minorVersion)
95
96    if int(minorVersion) % 2:
97        releaseVersion = re.search("wx_release_number=(\d+)", configureText).group(1)
98        versionText += ".%s" % (releaseVersion)
99
100    return versionText
101
102
103def getFrameworkName(options):
104    # the name of the framework is based on the wx port being built
105    name = "wxOSX"
106    if options.osx_cocoa:
107        name += "Cocoa"
108    else:
109        name += "Carbon"
110    return name
111
112
113def getPrefixInFramework(options, wxRoot=None):
114    # the path inside the framework that is the wx --prefix
115    fwPrefix = os.path.join(
116        os.path.abspath(options.mac_framework_prefix),
117        "%s.framework/Versions/%s" % (getFrameworkName(options), getWxRelease(wxRoot)))
118    return fwPrefix
119
120
121def macFixupInstallNames(destdir, prefix, buildDir=None):
122    # When an installdir is used then the install_names embedded in
123    # the dylibs are not correct.  Reset the IDs and the dependencies
124    # to use just the prefix.
125    print("**** macFixupInstallNames(%s, %s, %s)" % (destdir, prefix, buildDir))
126    pwd = os.getcwd()
127    os.chdir(destdir+prefix+'/lib')
128    dylibs = glob.glob('*.dylib')     # ('*[0-9].[0-9].[0-9].[0-9]*.dylib')
129    for lib in dylibs:
130        cmd = 'install_name_tool -id %s/lib/%s %s/lib/%s' % \
131              (prefix,lib,  destdir+prefix,lib)
132        print(cmd)
133        run(cmd)
134        for dep in dylibs:
135            if buildDir is not None:
136                cmd = 'install_name_tool -change %s/lib/%s %s/lib/%s %s/lib/%s' % \
137                      (buildDir,dep,  prefix,dep,  destdir+prefix,lib)
138            else:
139                cmd = 'install_name_tool -change %s/lib/%s %s/lib/%s %s/lib/%s' % \
140                      (destdir+prefix,dep,  prefix,dep,  destdir+prefix,lib)
141            print(cmd)
142            run(cmd)
143    os.chdir(pwd)
144
145
146def run(cmd):
147    global verbose
148    if verbose:
149        print("Running %s" % cmd)
150    return exitIfError(os.system(cmd), "Error running %s" % cmd)
151
152
153def getoutput(cmd):
154    sp = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
155    output = None
156    output = sp.stdout.read()
157    if sys.version_info > (3,):
158        output = output.decode('utf-8')  # TODO: is utf-8 okay here?
159    output = output.rstrip()
160    rval = sp.wait()
161    if rval:
162        # Failed!
163        print("Command '%s' failed with exit code %d." % (cmd, rval))
164        sys.exit(rval)
165    return output
166
167
168def main(scriptName, args):
169    global scriptDir
170    global wxRootDir
171    global contribDir
172    global options
173    global configure_opts
174    global wxBuilder
175    global nmakeCommand
176
177    scriptDir = os.path.dirname(os.path.abspath(scriptName))
178    wxRootDir = os.path.abspath(os.path.join(scriptDir, "..", ".."))
179
180    contribDir = os.path.join("contrib", "src")
181    installDir = None
182
183    VERSION = tuple([int(i) for i in getWxRelease().split('.')[:2]])
184
185    if sys.platform.startswith("win"):
186        contribDir = os.path.join(wxRootDir, "contrib", "build")
187
188    if sys.platform.startswith("win"):
189        toolkit = "msvc"
190    else:
191        toolkit = "autoconf"
192
193    defJobs = str(numCPUs())
194    defFwPrefix = '/Library/Frameworks'
195
196    option_dict = {
197        "clean"         : (False, "Clean all files from the build directory"),
198        "debug"         : (False, "Build the library in debug symbols"),
199        "builddir"      : ("", "Directory where the build will be performed for autoconf builds."),
200        "prefix"        : ("", "Configured prefix to use for autoconf builds. Defaults to installdir if set. Ignored for framework builds."),
201        "jobs"          : (defJobs, "Number of jobs to run at one time in make. Default: %s" % defJobs),
202        "install"       : (False, "Install the toolkit to the installdir directory, or the default dir."),
203        "installdir"    : ("", "Directory where built wxWidgets will be installed"),
204        "mac_distdir"   : (None, "If set on Mac, will create an installer package in the specified dir."),
205        "mac_universal_binary"
206                        : ("", "Comma separated list of architectures to include in the Mac universal binary"),
207        "mac_framework" : (False, "Install the Mac build as a framework"),
208        "mac_framework_prefix"
209                        : (defFwPrefix, "Prefix where the framework should be installed. Default: %s" % defFwPrefix),
210        "cairo"         : (False, "Enable dynamically loading the Cairo lib for wxGraphicsContext on MSW"),
211        "no_config"     : (False, "Turn off configure step on autoconf builds"),
212        "config_only"   : (False, "Only run the configure step and then exit"),
213        "rebake"        : (False, "Regenerate Bakefile and autoconf files"),
214        "unicode"       : (False, "Build the library with unicode support"),
215        "wxpython"      : (False, "Build the wxWidgets library with all options needed by wxPython"),
216        "cocoa"         : (False, "Build the old Mac Cocoa port."),
217        "osx_cocoa"     : (False, "Build the new Cocoa port"),
218        "shared"        : (False, "Build wx as a dynamic library"),
219        "extra_make"    : ("", "Extra args to pass on [n]make's command line."),
220        "features"      : ("", "A comma-separated list of wxUSE_XYZ defines on Win, or a list of configure flags on unix."),
221        "verbose"       : (False, "Print commands as they are run, (to aid with debugging this script)"),
222        "jom"           : (False, "Use jom.exe instead of nmake for MSW builds."),
223    }
224
225    parser = optparse.OptionParser(usage="usage: %prog [options]", version="%prog 1.0")
226
227    keys = option_dict.keys()
228    for opt in sorted(keys):
229        default = option_dict[opt][0]
230        action = "store"
231        if type(default) == bool:
232            action = "store_true"
233        parser.add_option("--" + opt, default=default, action=action, dest=opt,
234                          help=option_dict[opt][1])
235
236    options, arguments = parser.parse_args(args=args)
237
238    global verbose
239    if options.verbose:
240        verbose = True
241
242    # compiler / build system specific args
243    buildDir = options.builddir
244    args = []
245    installDir = options.installdir
246    prefixDir = options.prefix
247
248    if toolkit == "autoconf":
249        if not buildDir:
250            buildDir = os.getcwd()
251        configure_opts = []
252        if options.features != "":
253            configure_opts.extend(options.features.split(" "))
254
255        if options.unicode:
256            configure_opts.append("--enable-unicode")
257
258        if options.debug:
259            configure_opts.append("--enable-debug")
260
261        if options.cocoa:
262            configure_opts.append("--with-old_cocoa")
263
264        if options.osx_cocoa:
265            configure_opts.append("--with-osx_cocoa")
266
267        wxpy_configure_opts = [
268                            "--with-opengl",
269                            "--enable-sound",
270                            "--enable-graphics_ctx",
271                            "--enable-mediactrl",
272                            "--enable-display",
273                            "--enable-geometry",
274                            "--enable-debug_flag",
275                            "--enable-optimise",
276                            "--disable-debugreport",
277                            "--enable-uiactionsim",
278                            ]
279
280        if sys.platform.startswith("darwin"):
281            wxpy_configure_opts.append("--enable-monolithic")
282        else:
283            wxpy_configure_opts.append("--with-sdl")
284
285        # Try to use use lowest available SDK back to 10.5. Both Carbon and
286        # Cocoa builds require at least the 10.5 SDK now. We only add it to
287        # the wxpy options because this is a hard-requirement for wxPython,
288        # but other cases it is optional and is left up to the developer.
289        # TODO: there should be a command line option to set the SDK...
290        if sys.platform.startswith("darwin"):
291            for xcodePath in getXcodePaths():
292                sdks = [
293                    xcodePath+"/SDKs/MacOSX10.5.sdk",
294                    xcodePath+"/SDKs/MacOSX10.6.sdk",
295                    xcodePath+"/SDKs/MacOSX10.7.sdk",
296                    xcodePath+"/SDKs/MacOSX10.8.sdk",
297                    ]
298
299                # use the lowest available sdk
300                for sdk in sdks:
301                    if os.path.exists(sdk):
302                        wxpy_configure_opts.append(
303                            "--with-macosx-sdk=%s" % sdk)
304                        break
305
306        if not options.mac_framework:
307            if installDir and not prefixDir:
308                prefixDir = installDir
309            if prefixDir:
310                prefixDir = os.path.abspath(prefixDir)
311                configure_opts.append("--prefix=" + prefixDir)
312
313
314        if options.wxpython:
315            configure_opts.extend(wxpy_configure_opts)
316            if options.debug:
317                # wxPython likes adding these debug options too
318                configure_opts.append("--enable-debug_gdb")
319                configure_opts.append("--disable-optimise")
320                configure_opts.remove("--enable-optimise")
321
322
323        if options.rebake:
324            retval = run("make -f autogen.mk")
325            exitIfError(retval, "Error running autogen.mk")
326
327        if options.mac_framework:
328            # TODO: Should options.install be automatically turned on if the
329            # mac_framework flag is given?
330
331            # framework builds always need to be monolithic
332            if not "--enable-monolithic" in configure_opts:
333                configure_opts.append("--enable-monolithic")
334
335            # The --prefix given to configure will be the framework prefix
336            # plus the framework specific dir structure.
337            prefixDir = getPrefixInFramework(options)
338            configure_opts.append("--prefix=" + prefixDir)
339
340            # the framework build adds symlinks above the installDir + prefixDir folder
341            # so we need to wipe from the framework root instead of inside the prefixDir.
342            frameworkRootDir = os.path.abspath(os.path.join(installDir + prefixDir, "..", ".."))
343            if os.path.exists(frameworkRootDir):
344                if os.path.exists(frameworkRootDir):
345                    shutil.rmtree(frameworkRootDir)
346
347        if options.mac_universal_binary:
348            if options.mac_universal_binary == 'default':
349                if options.osx_cocoa:
350                    configure_opts.append("--enable-universal_binary=i386,x86_64")
351                else:
352                    configure_opts.append("--enable-universal_binary")
353            else:
354                configure_opts.append("--enable-universal_binary=%s" % options.mac_universal_binary)
355
356
357        print("Configure options: " + repr(configure_opts))
358        wxBuilder = builder.AutoconfBuilder()
359        if not options.no_config and not options.clean:
360            olddir = os.getcwd()
361            if buildDir:
362                os.chdir(buildDir)
363            exitIfError(wxBuilder.configure(dir=wxRootDir, options=configure_opts),
364                        "Error running configure")
365            os.chdir(olddir)
366
367        if options.config_only:
368            print("Exiting after configure")
369            return
370
371    elif toolkit in ["msvc", "msvcProject"]:
372        flags = {}
373        buildDir = os.path.abspath(os.path.join(scriptDir, "..", "msw"))
374
375        print("creating wx/msw/setup.h")
376        if options.unicode:
377            flags["wxUSE_UNICODE"] = "1"
378
379        if options.cairo:
380            if not os.environ.get("CAIRO_ROOT"):
381                print("WARNING: Expected CAIRO_ROOT set in the environment!")
382            flags["wxUSE_CAIRO"] = "1"
383
384        if options.wxpython:
385            flags["wxDIALOG_UNIT_COMPATIBILITY "] = "0"
386            flags["wxUSE_DEBUGREPORT"] = "0"
387            flags["wxUSE_DIALUP_MANAGER"] = "0"
388            flags["wxUSE_GRAPHICS_CONTEXT"] = "1"
389            flags["wxUSE_DISPLAY"] = "1"
390            flags["wxUSE_GLCANVAS"] = "1"
391            flags["wxUSE_POSTSCRIPT"] = "1"
392            flags["wxUSE_AFM_FOR_POSTSCRIPT"] = "0"
393            flags["wxUSE_DATEPICKCTRL_GENERIC"] = "1"
394
395            # Remove this when Windows XP finally dies, or when there is a
396            # solution for ticket #13116...
397            flags["wxUSE_COMPILER_TLS"] = "0"
398
399            if VERSION < (2,9):
400                flags["wxUSE_DIB_FOR_BITMAP"] = "1"
401
402            if VERSION >= (2,9):
403                flags["wxUSE_UIACTIONSIMULATOR"] = "1"
404
405
406        mswIncludeDir = os.path.join(wxRootDir, "include", "wx", "msw")
407        setupFile = os.path.join(mswIncludeDir, "setup.h")
408        setupText = open(setupFile, "rb").read()
409
410        for flag in flags:
411            setupText, subsMade = re.subn(flag + "\s+?\d", "%s %s" % (flag, flags[flag]), setupText)
412            if subsMade == 0:
413                print("Flag %s wasn't found in setup.h!" % flag)
414                sys.exit(1)
415
416        setupFile = open(os.path.join(mswIncludeDir, "setup.h"), "wb")
417        setupFile.write(setupText)
418        setupFile.close()
419        args = []
420        if toolkit == "msvc":
421            print("setting build options...")
422            args.append("-f makefile.vc")
423            if options.unicode:
424                args.append("UNICODE=1")
425
426            if options.wxpython:
427                args.append("OFFICIAL_BUILD=1")
428                args.append("COMPILER_VERSION=%s" % getVisCVersion())
429                args.append("SHARED=1")
430                args.append("MONOLITHIC=0")
431                args.append("USE_OPENGL=1")
432                args.append("USE_GDIPLUS=1")
433
434                if not options.debug:
435                    args.append("BUILD=release")
436                else:
437                    args.append("BUILD=debug")
438
439            if options.shared:
440                args.append("SHARED=1")
441
442            if options.cairo:
443                args.append(
444                    "CPPFLAGS=/I%s" %
445                     os.path.join(os.environ.get("CAIRO_ROOT", ""), 'include\\cairo'))
446
447            if options.jom:
448                nmakeCommand = 'jom.exe'
449
450            wxBuilder = builder.MSVCBuilder(commandName=nmakeCommand)
451
452        if toolkit == "msvcProject":
453            args = []
454            if options.shared or options.wxpython:
455                args.append("wx_dll.dsw")
456            else:
457                args.append("wx.dsw")
458
459            # TODO:
460            wxBuilder = builder.MSVCProjectBuilder()
461
462
463    if not wxBuilder:
464        print("Builder not available for your specified platform/compiler.")
465        sys.exit(1)
466
467    if options.clean:
468        print("Performing cleanup.")
469        wxBuilder.clean(dir=buildDir, options=args)
470
471        sys.exit(0)
472
473    if options.extra_make:
474        args.append(options.extra_make)
475
476    if not sys.platform.startswith("win"):
477        args.append("--jobs=" + options.jobs)
478    exitIfError(wxBuilder.build(dir=buildDir, options=args), "Error building")
479
480    if options.install:
481        extra=None
482        if installDir:
483            extra = ['DESTDIR='+installDir]
484        wxBuilder.install(dir=buildDir, options=extra)
485
486    if options.install and options.mac_framework:
487
488        def renameLibrary(libname, frameworkname):
489            reallib = libname
490            links = []
491            while os.path.islink(reallib):
492                links.append(reallib)
493                reallib = "lib/" + os.readlink(reallib)
494
495            #print("reallib is %s" % reallib)
496            run("mv -f %s lib/%s.dylib" % (reallib, frameworkname))
497
498            for link in links:
499                run("ln -s -f %s.dylib %s" % (frameworkname, link))
500
501        frameworkRootDir = prefixDir
502        if installDir:
503            print("installDir = %s" % installDir)
504            frameworkRootDir = installDir + prefixDir
505        os.chdir(frameworkRootDir)
506        build_string = ""
507        if options.debug:
508            build_string = "d"
509
510        fwname = getFrameworkName(options)
511        version = getoutput("bin/wx-config --release")
512        version_full = getoutput("bin/wx-config --version")
513        basename = getoutput("bin/wx-config --basename")
514        configname = getoutput("bin/wx-config --selected-config")
515
516        os.makedirs("Resources")
517        wxplist = dict(
518            CFBundleDevelopmentRegion="English",
519            CFBundleIdentifier='org.wxwidgets.wxosxcocoa',
520            CFBundleName=fwname,
521            CFBundleVersion=version_full,
522            CFBundleExecutable=fwname,
523            CFBundleGetInfoString="%s %s" % (fwname, version_full),
524            CFBundlePackageType="FMWK",
525            CFBundleSignature="WXCO",
526            CFBundleShortVersionString=version_full,
527            CFBundleInfoDictionaryVersion="6.0",
528        )
529
530        import plistlib
531        plistlib.writePlist(wxplist, os.path.join(frameworkRootDir, "Resources", "Info.plist"))
532
533        # we make wx the "actual" library file and link to it from libwhatever.dylib
534        # so that things can link to wx and survive minor version changes
535        renameLibrary("lib/lib%s-%s.dylib" % (basename, version), fwname)
536        run("ln -s -f lib/%s.dylib %s" % (fwname, fwname))
537
538        run("ln -s -f include Headers")
539
540        for lib in ["GL", "STC", "Gizmos", "Gizmos_xrc"]:
541            libfile = "lib/lib%s_%s-%s.dylib" % (basename, lib.lower(), version)
542            if os.path.exists(libfile):
543                frameworkDir = "framework/wx%s/%s" % (lib, version)
544                if not os.path.exists(frameworkDir):
545                    os.makedirs(frameworkDir)
546                renameLibrary(libfile, "wx" + lib)
547                run("ln -s -f ../../../%s %s/wx%s" % (libfile, frameworkDir, lib))
548
549        for lib in glob.glob("lib/*.dylib"):
550            if not os.path.islink(lib):
551                corelibname = "lib/lib%s-%s.0.dylib" % (basename, version)
552                run("install_name_tool -id %s %s" % (os.path.join(prefixDir, lib), lib))
553                run("install_name_tool -change %s %s %s" % (os.path.join(frameworkRootDir, corelibname), os.path.join(prefixDir, corelibname), lib))
554
555        os.chdir("include")
556
557        header_template = """
558#ifndef __WX_FRAMEWORK_HEADER__
559#define __WX_FRAMEWORK_HEADER__
560
561%s
562
563#endif // __WX_FRAMEWORK_HEADER__
564"""
565        headers = ""
566        header_dir = "wx-%s/wx" % version
567        for include in glob.glob(header_dir + "/*.h"):
568            headers += "#include <wx/" + os.path.basename(include) + ">\n"
569
570        framework_header = open("%s.h" % fwname, "w")
571        framework_header.write(header_template % headers)
572        framework_header.close()
573
574        run("ln -s -f %s wx" % header_dir)
575        os.chdir("wx-%s/wx" % version)
576        run("ln -s -f ../../../lib/wx/include/%s/wx/setup.h setup.h" % configname)
577
578        os.chdir(os.path.join(frameworkRootDir, ".."))
579        run("ln -s -f %s Current" % getWxRelease())
580        os.chdir("..")
581        run("ln -s -f Versions/Current/Headers Headers")
582        run("ln -s -f Versions/Current/Resources Resources")
583        run("ln -s -f Versions/Current/%s %s" % (fwname, fwname))
584
585        # sanity check to ensure the symlink works
586        os.chdir("Versions/Current")
587
588        # put info about the framework into wx-config
589        os.chdir(frameworkRootDir)
590        text = file('lib/wx/config/%s' % configname).read()
591        text = text.replace("MAC_FRAMEWORK=", "MAC_FRAMEWORK=%s" % getFrameworkName(options))
592        if options.mac_framework_prefix not in ['/Library/Frameworks',
593                                                '/System/Library/Frameworks']:
594            text = text.replace("MAC_FRAMEWORK_PREFIX=",
595                         "MAC_FRAMEWORK_PREFIX=%s" % options.mac_framework_prefix)
596        file('lib/wx/config/%s' % configname, 'w').write(text)
597
598        # The framework is finished!
599        print("wxWidgets framework created at: " +
600              os.path.join( installDir,
601                            options.mac_framework_prefix,
602                            '%s.framework' % fwname))
603
604
605    # adjust the install_name if needed
606    if sys.platform.startswith("darwin") and \
607           options.install and \
608           options.installdir and \
609           not options.mac_framework and \
610           not options.wxpython:  # wxPython's build will do this later if needed
611        if not prefixDir:
612            prefixDir = '/usr/local'
613        macFixupInstallNames(options.installdir, prefixDir)#, buildDir)
614
615    # make a package if a destdir was set.
616    if options.mac_framework and \
617            options.install and \
618            options.installdir and \
619            options.mac_distdir:
620
621        if os.path.exists(options.mac_distdir):
622            shutil.rmtree(options.mac_distdir)
623
624        packagedir = os.path.join(options.mac_distdir, "packages")
625        os.makedirs(packagedir)
626        basename = os.path.basename(prefixDir.split(".")[0])
627        packageName = basename + "-" + getWxRelease()
628        packageMakerPath = getXcodePaths()[0]+"/usr/bin/packagemaker "
629        args = []
630        args.append("--root %s" % options.installdir)
631        args.append("--id org.wxwidgets.%s" % basename.lower())
632        args.append("--title %s" % packageName)
633        args.append("--version %s" % getWxRelease())
634        args.append("--out %s" % os.path.join(packagedir, packageName + ".pkg"))
635        cmd = packageMakerPath + ' '.join(args)
636        print("cmd = %s" % cmd)
637        run(cmd)
638
639        os.chdir(options.mac_distdir)
640
641        run('hdiutil create -srcfolder %s -volname "%s" -imagekey zlib-level=9 %s.dmg' % (packagedir, packageName, packageName))
642
643        shutil.rmtree(packagedir)
644
645if __name__ == '__main__':
646    exitWithException = False  # use sys.exit instead
647    main(sys.argv[0], sys.argv[1:])
648
649