[bockbuild] Moved Gtk+ files back to main bockbuild repo
[mono.git] / bockbuild / mac-sdk / profile.py
1 import itertools
2 import os
3 import re
4 import shutil
5 import string
6 import sys
7 import tempfile
8 import subprocess
9 import stat
10
11 from bockbuild.darwinprofile import DarwinProfile
12 from bockbuild.util.util import *
13 from glob import glob
14
15 class MonoReleaseProfile(DarwinProfile):
16
17     packages = [
18         'gettext',
19         'pkg-config',
20
21         # Base Libraries
22         'libpng',
23         'libjpeg',
24         'libtiff',
25         'libgif',
26         'libxml2',
27         'freetype',
28         'fontconfig',
29         'pixman',
30         'cairo',
31         'libffi',
32         'glib',
33         'pango',
34         'atk',
35         'intltool',
36         'gdk-pixbuf',
37         'gtk+',
38         'libglade',
39         'sqlite',
40         'expat',
41         'ige-mac-integration',
42
43         # Theme
44         'libcroco',
45         'librsvg',
46         'hicolor-icon-theme',
47         'gtk-engines',
48         'murrine',
49         'xamarin-gtk-theme',
50         'gtk-quartz-engine',
51
52         # Mono
53         'mono-llvm',
54         'mono',
55         'msbuild',
56         'pcl-reference-assemblies',
57         'libgdiplus',
58         'xsp',
59         'gtk-sharp',
60         'ironlangs',
61         'fsharp',
62         'mono-basic',
63         'nuget'
64     ]
65
66     def attach (self, bockbuild):
67         self.min_version = 7
68         DarwinProfile.attach (self, bockbuild)
69
70         # quick disk space check (http://stackoverflow.com/questions/787776/)
71         s = os.statvfs(bockbuild.root)
72         free_space = (s.f_bavail * s.f_frsize) / (1024 * 1024 * 1024)  # in GB
73
74         if free_space < 10:
75             error('Low disk space (less than 10GB), aborting')
76
77         # check for XQuartz installation (needed for libgdiplus)
78         if not os.path.exists('/opt/X11/include/X11/Xlib.h'):
79             error(
80                 'XQuartz is required to be installed (download from http://xquartz.macosforge.org/) ')
81
82         self.MONO_ROOT = "/Library/Frameworks/Mono.framework"
83         self.BUILD_NUMBER = "0"
84         self.MDK_GUID = "964ebddd-1ffe-47e7-8128-5ce17ffffb05"
85
86         self.self_dir = os.path.realpath(os.path.dirname(sys.argv[0]))
87         self.packaging_dir = os.path.join(self.self_dir, "packaging")
88
89         system_mono_dir = '/Library/Frameworks/Mono.framework/Versions/Current'
90         self.env.set('system_mono', os.path.join(
91             system_mono_dir, 'bin', 'mono'))
92         self.env.set('system_mcs', os.path.join(system_mono_dir, 'bin', 'mcs'))
93
94         self.env.set('system_mono_version', backtick(
95             '%s --version' % self.env.system_mono)[0])
96
97         # config overrides for some programs to be functional while staged
98
99         self.env.set('GDK_PIXBUF_MODULE_FILE',
100                      '%{staged_prefix}/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache')
101         self.env.set('GDK_PIXBUF_MODULEDIR',
102                      '%{staged_prefix}/lib/gdk-pixbuf-2.0/2.10.0/loaders')
103         self.env.set('PANGO_SYSCONFDIR', '%{staged_prefix}/etc')
104         self.env.set('PANGO_LIBDIR', '%{staged_prefix}/lib')
105         # self.env.set ('MONO_PATH', '%{staged_prefix}/lib/mono/4.0')
106         self.debug_info = ['gtk+', 'cairo',
107                            'pango', 'mono', 'llvm', 'libgdiplus']
108         self.cache_host = None
109
110     def setup_release(self):
111         self.mono_package = self.release_packages['mono']
112         self.mono_package.fetch()
113
114         verbose('Mono version: %s' % self.mono_package.version)
115         self.RELEASE_VERSION = self.mono_package.version
116         self.prefix = os.path.join(
117             self.MONO_ROOT, "Versions", self.RELEASE_VERSION)
118
119         if os.path.exists(self.prefix):
120             error('Prefix %s exists, and may interfere with the staged build. Please remove and try again.' % self.prefix)
121
122         self.calculate_updateid()
123         trace(self.package_info('MDK'))
124
125         self.dont_optimize = ['pixman']
126
127         for p in self.release_packages.values():
128             if p.name in self.dont_optimize:
129                 continue
130             self.gcc_flags.extend(['-O2'])
131
132     # THIS IS THE MAIN METHOD FOR MAKING A PACKAGE
133     def package(self):
134         self.fix_gtksharp_configs()
135         self.verify_binaries()
136
137         working = self.setup_working_dir()
138         uninstall_script = os.path.join(working, "uninstallMono.sh")
139
140         # make the MDK
141         self.apply_blacklist(working, 'mdk_blacklist.sh')
142         self.make_updateinfo(working, self.MDK_GUID)
143         mdk_pkg = self.run_pkgbuild(working, "MDK")
144         title(mdk_pkg)
145         # self.make_dmg(mdk_dmg, title, mdk_pkg, uninstall_script)
146
147         shutil.rmtree(working)
148
149     def calculate_updateid(self):
150         # Create the updateid
151         pwd = os.getcwd()
152         git_bin = self.bockbuild.git_bin
153         trace("cur path is %s and git is %s" % (pwd, git_bin))
154         blame_rev_str = 'cd %s; %s blame configure.ac HEAD | grep AC_INIT | sed \'s/ .*//\' ' % (
155             self.mono_package.workspace, git_bin)
156         blame_rev = backtick(blame_rev_str)[0]
157         trace("Last commit to the version string %s" % (blame_rev))
158         version_number_str = 'cd %s; %s log %s..HEAD --oneline | wc -l | sed \'s/ //g\'' % (
159             self.mono_package.workspace, git_bin, blame_rev)
160         self.BUILD_NUMBER = backtick(version_number_str)[0]
161         trace("Calculating commit distance, %s" % (self.BUILD_NUMBER))
162         self.FULL_VERSION = self.RELEASE_VERSION + "." + self.BUILD_NUMBER
163         os.chdir(pwd)
164
165         parts = self.RELEASE_VERSION.split(".")
166         version_list = (parts + ["0"] * (3 - len(parts)))[:4]
167         for i in range(1, 3):
168             version_list[i] = version_list[i].zfill(2)
169             self.updateid = "".join(version_list)
170             self.updateid += self.BUILD_NUMBER.replace(
171                 ".", "").zfill(9 - len(self.updateid))
172         trace(self.updateid)
173
174     # creates and returns the path to a working directory containing:
175     #   PKGROOT/ - this root will be bundled into the .pkg and extracted at /
176     #   uninstallMono.sh - copied onto the DMG
177     #   Info{_sdk}.plist - used by packagemaker to make the installer
178     #   resources/ - other resources used by packagemaker for the installer
179     def setup_working_dir(self):
180         def make_package_symlinks(root):
181             os.symlink(self.prefix, os.path.join(root, "Versions", "Current"))
182             currentlink = os.path.join(self.MONO_ROOT, "Versions", "Current")
183             links = [
184                 ("bin", "Commands"),
185                 ("include", "Headers"),
186                 ("lib", "Libraries"),
187                 ("", "Home"),
188                 (os.path.join("lib", "libmono-2.0.dylib"), "Mono")
189             ]
190             for srcname, destname in links:
191                 src = os.path.join(currentlink, srcname)
192                 dest = os.path.join(root, destname)
193                 # If the symlink exists, we remove it so we can create a fresh
194                 # one
195                 if os.path.exists(dest):
196                     os.unlink(dest)
197                 os.symlink(src, dest)
198
199         tmpdir = tempfile.mkdtemp()
200         monoroot = os.path.join(tmpdir, "PKGROOT", self.MONO_ROOT[1:])
201         versions = os.path.join(monoroot, "Versions")
202         os.makedirs(versions)
203
204         print "Setting up temporary package directory:", tmpdir
205
206         # setup metadata
207         run_shell('rsync -aPq %s/* %s' % (self.packaging_dir, tmpdir), False)
208
209         packages_list = string.join(
210             [pkg.desc for pkg in self.release_packages.values()], "\\\n")
211         deps_list = 'bockbuild (rev. %s)\\\n' % self.bockbuild_rev + string.join(
212             [pkg.desc for pkg in self.toolchain_packages.values()], "\\\n")
213
214         parameter_map = {
215             '@@MONO_VERSION@@': self.RELEASE_VERSION,
216             '@@MONO_RELEASE@@': self.BUILD_NUMBER,
217             '@@MONO_VERSION_RELEASE@@': self.RELEASE_VERSION + '_' + self.BUILD_NUMBER,
218             '@@MONO_CSDK_GUID@@': self.MDK_GUID,
219             '@@MONO_VERSION_RELEASE_INT@@': self.updateid,
220             '@@PACKAGES@@': packages_list,
221             '@@DEP_PACKAGES@@': deps_list
222         }
223         for dirpath, d, files in os.walk(tmpdir):
224             for name in files:
225                 if not name.startswith('.'):
226                     replace_in_file(os.path.join(dirpath, name), parameter_map)
227
228         make_package_symlinks(monoroot)
229
230         # copy to package root
231         run_shell('rsync -aPq "%s"/* "%s/%s"' %
232                   (self.package_root, versions, self.RELEASE_VERSION), False)
233
234         return tmpdir
235
236     def apply_blacklist(self, working_dir, blacklist_name):
237         print "Applying blacklist script:", blacklist_name
238         blacklist = os.path.join(self.packaging_dir, blacklist_name)
239         root = os.path.join(working_dir, "PKGROOT", self.prefix[1:])
240         run_shell('%s "%s" > /dev/null' % (blacklist, root), print_cmd=False)
241
242     def run_pkgbuild(self, working_dir, package_type):
243         print 'Running pkgbuild & productbuild...',
244         info = self.package_info(package_type)
245         output = os.path.join(self.self_dir, info["filename"])
246         identifier = "com.xamarin.mono-" + info["type"] + ".pkg"
247         resources_dir = os.path.join(working_dir, "resources")
248         distribution_xml = os.path.join(resources_dir, "distribution.xml")
249
250         old_cwd = os.getcwd()
251         os.chdir(working_dir)
252         pkgbuild = "/usr/bin/pkgbuild"
253         pkgbuild_cmd = ' '.join([pkgbuild,
254                                  "--identifier " + identifier,
255                                  "--root '%s/PKGROOT'" % working_dir,
256                                  "--version '%s'" % self.RELEASE_VERSION,
257                                  "--install-location '/'",
258                                  "--scripts '%s'" % resources_dir,
259                                  "--quiet",
260                                  os.path.join(working_dir, "mono.pkg")])
261
262         run_shell(pkgbuild_cmd)
263
264         productbuild = "/usr/bin/productbuild"
265         productbuild_cmd = ' '.join([productbuild,
266                                      "--resources %s" % resources_dir,
267                                      "--distribution %s" % distribution_xml,
268                                      "--package-path %s" % working_dir,
269                                      "--quiet",
270                                      output])
271
272         run_shell(productbuild_cmd)
273
274         assert_exists(output)
275         os.chdir(old_cwd)
276         print output
277         return output
278
279     def make_updateinfo(self, working_dir, guid):
280         updateinfo = os.path.join(
281             working_dir, "PKGROOT", self.prefix[1:], "updateinfo")
282         with open(updateinfo, "w") as updateinfo:
283             updateinfo.write(guid + ' ' + self.updateid + "\n")
284
285     def package_info(self, pkg_type):
286         arch = self.bockbuild.cmd_options.arch
287         arch_str = None
288         if  arch == "darwin-32":
289             arch_str = "x86"
290         elif arch == "darwin-64":
291             arch_str = "x64"
292         elif arch == "darwin-universal":
293             arch_str = "universal"
294         else:
295             error ("Unknown architecture")
296
297         if self.bockbuild.cmd_options.release_build:
298             info = (pkg_type, self.FULL_VERSION, arch_str)
299         else:
300             info = (pkg_type, '%s-%s' % (git_shortid(self.bockbuild,
301                                                      self.mono_package.workspace), self.FULL_VERSION), arch_str)
302
303         filename = "MonoFramework-%s-%s.macos10.xamarin.%s.pkg" % info
304         return {
305             "type": pkg_type,
306             "filename": filename
307         }
308
309     def fix_line(self, line, matcher):
310         def insert_install_root(matches):
311             root = self.prefix
312             captures = matches.groupdict()
313             return 'target="%s"' % os.path.join(root, "lib", captures["lib"])
314
315         if matcher(line):
316             pattern = r'target="(?P<lib>.+\.dylib)"'
317             result = re.sub(pattern, insert_install_root, line)
318             return result
319         else:
320             return line
321
322     def fix_dllmap(self, config, matcher):
323         handle, temp = tempfile.mkstemp()
324         with open(config) as c:
325             with open(temp, "w") as output:
326                 for line in c:
327                     output.write(self.fix_line(line, matcher))
328         os.rename(temp, config)
329         os.system('chmod a+r %s' % config)
330
331     def fix_gtksharp_configs(self):
332         print 'Fixing GTK# configuration files...',
333         count = 0
334         libs = [
335             'atk-sharp',
336             'gdk-sharp',
337             'glade-sharp',
338             'glib-sharp',
339             'gtk-dotnet',
340             'gtk-sharp',
341             'pango-sharp'
342         ]
343         gac = os.path.join(self.package_root, "lib", "mono", "gac")
344         confs = [glob(os.path.join(gac, x, "*", "*.dll.config")) for x in libs]
345         for c in itertools.chain(*confs):
346             count = count + 1
347             self.fix_dllmap(c, lambda line: "dllmap" in line)
348         print count
349
350     def verify(self, f):
351         result = " ".join(backtick("otool -L " + f))
352         regex = os.path.join(self.MONO_ROOT, "Versions", r"(\d+\.\d+\.\d+)")
353
354         match = re.search(regex, result)
355         if match is None:
356             return
357         token = match.group(1)
358         trace(token)
359         if self.RELEASE_VERSION not in token:
360             raise Exception("%s references Mono %s\n%s" % (f, token, text))
361
362     def verify_binaries(self):
363         bindir = os.path.join(self.package_root, "bin")
364         for path, dirs, files in os.walk(bindir):
365             for name in files:
366                 f = os.path.join(path, name)
367                 file_type = backtick('file "%s"' % f)
368                 if "Mach-O executable" in "".join(file_type):
369                     self.verify(f)
370
371     def shell(self):
372         envscript = '''#!/bin/sh
373         PROFNAME="%s"
374         INSTALLDIR="%s"
375         ROOT="%s"
376         export DYLD_FALLBACK_LIBRARY_PATH="$INSTALLDIR/lib:/lib:/usr/lib"
377         export ACLOCAL_PATH="$INSTALLDIR/share/aclocal"
378         export CONFIG_SITE="$INSTALLDIR/$PROFNAME-config.site"
379         export MONO_GAC_PREFIX="$INSTALLDIR"
380         export MONO_ADDINS_REGISTRY="$ROOT/addinreg"
381         export MONO_INSTALL_PREFIX="$INSTALLDIR"
382
383         export PS1="\[\e[1;3m\][$PROFNAME] \w @ "
384         bash -i
385         ''' % (self.profile_name, self.staged_prefix, self.root)
386
387         path = os.path.join(self.root, self.profile_name + '.sh')
388
389         with open(path, 'w') as f:
390             f.write(envscript)
391
392         os.chmod(path, os.stat(path).st_mode | stat.S_IEXEC)
393
394         subprocess.call(['bash', '-c', path])
395
396 MonoReleaseProfile()