9e8c2c6736f3d46a75c658cd304b70b678f05330
[mono.git] / packaging / MacSDK / 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     description = 'The Mono Framework for MacOS'
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         system_mono_dir = '/Library/Frameworks/Mono.framework/Versions/Current'
87         self.env.set('system_mono', os.path.join(
88             system_mono_dir, 'bin', 'mono'))
89         self.env.set('system_mcs', os.path.join(system_mono_dir, 'bin', 'mcs'))
90
91         self.env.set('system_mono_version', backtick(
92             '%s --version' % self.env.system_mono)[0])
93
94         # config overrides for some programs to be functional while staged
95
96         self.env.set('GDK_PIXBUF_MODULE_FILE',
97                      '%{staged_prefix}/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache')
98         self.env.set('GDK_PIXBUF_MODULEDIR',
99                      '%{staged_prefix}/lib/gdk-pixbuf-2.0/2.10.0/loaders')
100         self.env.set('PANGO_SYSCONFDIR', '%{staged_prefix}/etc')
101         self.env.set('PANGO_LIBDIR', '%{staged_prefix}/lib')
102         # self.env.set ('MONO_PATH', '%{staged_prefix}/lib/mono/4.0')
103         self.debug_info = ['gtk+', 'cairo',
104                            'pango', 'mono', 'llvm', 'libgdiplus']
105         self.cache_host = None
106
107     def setup_release(self):
108         self.mono_package = self.release_packages['mono']
109         dest = os.path.join(self.bockbuild.build_root, self.mono_package.source_dir_name)
110         self.mono_package.fetch(dest)
111
112
113         verbose('Mono version: %s' % self.mono_package.version)
114         self.RELEASE_VERSION = self.mono_package.version
115         self.prefix = os.path.join(
116             self.MONO_ROOT, "Versions", self.RELEASE_VERSION)
117
118         if os.path.exists(self.prefix):
119             error('Prefix %s exists, and may interfere with the staged build. Please remove and try again.' % self.prefix)
120
121         self.calculate_updateid()
122         trace(self.package_info('MDK'))
123
124         self.dont_optimize = ['pixman']
125
126         for p in self.release_packages.values():
127             if p.name in self.dont_optimize:
128                 continue
129             self.gcc_flags.extend(['-O2'])
130
131     # THIS IS THE MAIN METHOD FOR MAKING A PACKAGE
132     def package(self):
133         self.fix_gtksharp_configs()
134         self.verify_binaries()
135
136         working = self.setup_working_dir()
137         uninstall_script = os.path.join(working, "uninstallMono.sh")
138
139         # make the MDK
140         self.apply_blacklist(working, 'mdk_blacklist.sh')
141         self.make_updateinfo(working, self.MDK_GUID)
142         mdk_pkg = self.run_pkgbuild(working, "MDK")
143         title(mdk_pkg)
144         # self.make_dmg(mdk_dmg, title, mdk_pkg, uninstall_script)
145
146         shutil.rmtree(working)
147
148     def calculate_updateid(self):
149         # Create the updateid
150         pwd = os.getcwd()
151         git_bin = self.bockbuild.git_bin
152         trace("cur path is %s and git is %s" % (pwd, git_bin))
153         blame_rev_str = 'cd %s; %s blame configure.ac HEAD | grep AC_INIT | sed \'s/ .*//\' ' % (
154             self.mono_package.workspace, git_bin)
155         blame_rev = backtick(blame_rev_str)[0]
156         trace("Last commit to the version string %s" % (blame_rev))
157         version_number_str = 'cd %s; %s log %s..HEAD --oneline | wc -l | sed \'s/ //g\'' % (
158             self.mono_package.workspace, git_bin, blame_rev)
159         self.BUILD_NUMBER = backtick(version_number_str)[0]
160         trace("Calculating commit distance, %s" % (self.BUILD_NUMBER))
161         self.FULL_VERSION = self.RELEASE_VERSION + "." + self.BUILD_NUMBER
162         os.chdir(pwd)
163
164         parts = self.RELEASE_VERSION.split(".")
165         version_list = (parts + ["0"] * (3 - len(parts)))[:4]
166         for i in range(1, 3):
167             version_list[i] = version_list[i].zfill(2)
168             self.updateid = "".join(version_list)
169             self.updateid += self.BUILD_NUMBER.replace(
170                 ".", "").zfill(9 - len(self.updateid))
171         trace(self.updateid)
172
173     # creates and returns the path to a working directory containing:
174     #   PKGROOT/ - this root will be bundled into the .pkg and extracted at /
175     #   uninstallMono.sh - copied onto the DMG
176     #   Info{_sdk}.plist - used by packagemaker to make the installer
177     #   resources/ - other resources used by packagemaker for the installer
178     def setup_working_dir(self):
179         def make_package_symlinks(root):
180             os.symlink(self.prefix, os.path.join(root, "Versions", "Current"))
181             currentlink = os.path.join(self.MONO_ROOT, "Versions", "Current")
182             links = [
183                 ("bin", "Commands"),
184                 ("include", "Headers"),
185                 ("lib", "Libraries"),
186                 ("", "Home"),
187                 (os.path.join("lib", "libmono-2.0.dylib"), "Mono")
188             ]
189             for srcname, destname in links:
190                 src = os.path.join(currentlink, srcname)
191                 dest = os.path.join(root, destname)
192                 # If the symlink exists, we remove it so we can create a fresh
193                 # one
194                 if os.path.exists(dest):
195                     os.unlink(dest)
196                 os.symlink(src, dest)
197
198         tmpdir = tempfile.mkdtemp()
199         monoroot = os.path.join(tmpdir, "PKGROOT", self.MONO_ROOT[1:])
200         versions = os.path.join(monoroot, "Versions")
201         os.makedirs(versions)
202
203         print "Setting up temporary package directory:", tmpdir
204
205         # setup metadata
206         self.packaging_dir = os.path.join(self.path, "packaging")
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' % bockbuild.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                   (bockbuild.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.resource_path, 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(bockbuild.package_root, "lib", "mono", "gac")
344         confs = [glob.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(bockbuild.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()