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