1 # Copyright (C) 2006, Thomas Leonard
2 # See http://0install.net/0compile.html
4 import sys
, os
, __main__
, time
, shutil
, glob
, codecs
, subprocess
5 from os
.path
import join
6 from logging
import info
, warn
7 from xml
.dom
import minidom
, XMLNS_NAMESPACE
8 from optparse
import OptionParser
11 from zeroinstall
import SafeException
12 from zeroinstall
.injector
import model
, namespaces
13 from zeroinstall
.injector
.iface_cache
import iface_cache
15 from support
import BuildEnv
, ensure_dir
, XMLNS_0COMPILE
, is_package_impl
, parse_bool
, depth
16 from support
import spawn_and_check
, find_in_path
, ENV_FILE
, lookup
, spawn_maybe_sandboxed
18 if hasattr(os
.path
, 'relpath'):
19 relpath
= os
.path
.relpath
21 # Copied from Python 2.6 (GPL compatible license)
22 # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006 Python Software Foundation; All Rights Reserved
24 # Return the longest prefix of all list elements.
26 "Given a list of pathnames, returns the longest common leading component"
30 for i
, c
in enumerate(s1
):
35 def relpath(path
, start
):
36 """Return a relative version of a path"""
39 raise ValueError("no path specified")
41 start_list
= os
.path
.abspath(start
).split('/')
42 path_list
= os
.path
.abspath(path
).split('/')
44 # Work out how much of the filepath is shared by start and path.
45 i
= len(commonprefix([start_list
, path_list
]))
47 rel_list
= ['..'] * (len(start_list
)-i
) + path_list
[i
:]
50 return join(*rel_list
)
52 # If we have to modify any pkg-config files, we put the new versions in $TMPDIR/PKG_CONFIG_OVERRIDES
53 PKG_CONFIG_OVERRIDES
= 'pkg-config-overrides'
56 os
.environ
[name
] = value
57 print "%s=%s" % (name
, value
)
59 def do_env_binding(binding
, path
):
60 env(binding
.name
, binding
.get_value(path
, os
.environ
.get(binding
.name
, None)))
62 def correct_for_64bit(base
, rel_path
):
63 """If rel_path starts lib or usr/lib and doesn't exist, try with lib64 instead."""
64 if os
.path
.exists(os
.path
.join(base
, rel_path
)):
67 if rel_path
.startswith('lib/') or rel_path
.startswith('usr/lib/'):
68 new_rel_path
= rel_path
.replace('lib/', 'lib64/', 1)
69 if os
.path
.exists(os
.path
.join(base
, new_rel_path
)):
74 def write_pc(name
, lines
):
75 overrides_dir
= os
.path
.join(os
.environ
['TMPDIR'], PKG_CONFIG_OVERRIDES
)
76 if not os
.path
.isdir(overrides_dir
):
77 os
.mkdir(overrides_dir
)
78 stream
= open(os
.path
.join(overrides_dir
, name
), 'w')
79 stream
.write(''.join(lines
))
82 def do_pkg_config_binding(binding
, impl
):
83 feed_name
= impl
.feed
.split('/')[-1]
85 new_insert
= correct_for_64bit(path
, binding
.insert
)
86 if new_insert
!= binding
.insert
:
87 print "PKG_CONFIG_PATH dir <%s>/%s not found; using %s instead" % (feed_name
, binding
.insert
, new_insert
)
88 binding
= model
.EnvironmentBinding(binding
.name
,
93 orig_path
= os
.path
.join(path
, binding
.insert
)
94 if os
.path
.isdir(orig_path
):
95 for pc
in os
.listdir(orig_path
):
96 stream
= open(os
.path
.join(orig_path
, pc
))
97 lines
= stream
.readlines()
99 for i
, line
in enumerate(lines
):
100 if '=' not in line
: continue
101 name
, value
= [x
.strip() for x
in line
.split('=', 1)]
102 if name
== 'prefix' and value
.startswith('/'):
103 print "Absolute prefix=%s in %s; overriding..." % (value
, feed_name
)
104 lines
[i
] = 'prefix=%s/%s\n' % (path
, value
[1:])
107 do_env_binding(binding
, path
)
109 def fixup_generated_pkgconfig_file(pc_file
):
110 stream
= open(pc_file
)
111 lines
= stream
.readlines()
113 for i
, line
in enumerate(lines
):
114 if '=' not in line
: continue
115 name
, value
= [x
.strip() for x
in line
.split('=', 1)]
116 if name
== 'prefix' and value
.startswith('/'):
117 print "Absolute prefix=%s in %s; fixing..." % (value
, pc_file
)
118 rel_path
= relpath(value
, os
.path
.dirname(pc_file
)) # Requires Python 2.6
119 lines
[i
] = 'prefix=${pcfiledir}/%s\n' % rel_path
120 write_pc(pc_file
, lines
)
123 # After doing a build, check that we didn't generate pkgconfig files with absolute paths
125 def fixup_generated_pkgconfig_files():
126 for root
, dirs
, files
in os
.walk(os
.environ
['DISTDIR']):
127 if os
.path
.basename(root
) == 'pkgconfig':
129 if f
.endswith('.pc'):
130 info("Checking generated pkgconfig file '%s'", f
)
131 fixup_generated_pkgconfig_file(os
.path
.join(root
, f
))
133 def remove_la_file(path
):
134 # Read the contents...
139 # Check it really is a libtool archive...
140 if 'Please DO NOT delete this file' not in data
:
141 warn("Ignoring %s; doesn't look like a libtool archive", path
)
145 print "Removed %s (.la files contain absolute paths)" % path
147 # libtool archives contain hard-coded paths. Lucky, modern systems don't need them, so remove
149 def remove_la_files():
150 for root
, dirs
, files
in os
.walk(os
.environ
['DISTDIR']):
151 if os
.path
.basename(root
) == 'lib':
153 if f
.endswith('.la'):
154 remove_la_file(os
.path
.join(root
, f
))
156 warn("Found static archive '%s'; maybe build with --disable-static?", f
)
158 def do_build_internal(options
, args
):
160 # If a sandbox is being used, we're in it now.
161 import getpass
, socket
163 buildenv
= BuildEnv()
164 sels
= buildenv
.get_selections()
166 builddir
= os
.path
.realpath('build')
167 ensure_dir(buildenv
.metadir
)
169 build_env_xml
= join(buildenv
.metadir
, 'build-environment.xml')
171 buildenv_doc
= buildenv
.get_selections().toDOM()
173 # Create build-environment.xml file
174 root
= buildenv_doc
.documentElement
175 info
= buildenv_doc
.createElementNS(XMLNS_0COMPILE
, 'build-info')
176 root
.appendChild(info
)
177 info
.setAttributeNS(None, 'time', time
.strftime('%Y-%m-%d %H:%M').strip())
178 info
.setAttributeNS(None, 'host', socket
.getfqdn())
179 info
.setAttributeNS(None, 'user', getpass
.getuser())
181 info
.setAttributeNS(None, 'arch', '%s-%s' % (uname
[0], uname
[4]))
182 stream
= file(build_env_xml
, 'w')
183 buildenv_doc
.writexml(stream
, addindent
=" ", newl
="\n")
186 # Create local binary interface file
187 src_iface
= iface_cache
.get_interface(buildenv
.interface
)
188 src_impl
= buildenv
.chosen_impl(buildenv
.interface
)
189 write_sample_interface(buildenv
, src_iface
, src_impl
)
191 # Check 0compile is new enough
192 min_version
= model
.parse_version(src_impl
.attrs
.get(XMLNS_0COMPILE
+ ' min-version', None))
193 if min_version
and min_version
> model
.parse_version(__main__
.version
):
194 raise SafeException("%s-%s requires 0compile >= %s, but we are only version %s" %
195 (src_iface
.get_name(), src_impl
.version
, model
.format_version(min_version
), __main__
.version
))
198 patch_file
= join(buildenv
.metadir
, 'from-%s.patch' % src_impl
.version
)
199 if buildenv
.user_srcdir
:
200 # (ignore errors; will already be shown on stderr)
201 os
.system("diff -urN '%s' src > %s" %
202 (buildenv
.orig_srcdir
.replace('\\', '\\\\').replace("'", "\\'"),
204 if os
.path
.getsize(patch_file
) == 0:
205 os
.unlink(patch_file
)
206 elif os
.path
.exists(patch_file
):
207 os
.unlink(patch_file
)
209 env('BUILDDIR', builddir
)
210 env('DISTDIR', buildenv
.distdir
)
211 env('SRCDIR', buildenv
.user_srcdir
or buildenv
.orig_srcdir
)
215 for needed_iface
in sels
.selections
:
216 impl
= buildenv
.chosen_impl(needed_iface
)
219 def process_bindings(bindings
, dep_impl
):
220 if dep_impl
.id.startswith('package:'):
223 if isinstance(b
, model
.EnvironmentBinding
):
224 if b
.name
== 'PKG_CONFIG_PATH':
225 do_pkg_config_binding(b
, dep_impl
)
227 do_env_binding(b
, lookup(dep_impl
))
229 # Bindings that tell this component how to find itself...
230 process_bindings(impl
.bindings
, impl
)
232 # Bindings that tell this component how to find its dependencies...
233 for dep
in impl
.dependencies
:
234 dep_impl
= buildenv
.chosen_impl(dep
.interface
)
235 process_bindings(dep
.bindings
, dep_impl
)
237 # These mappings are needed when mixing Zero Install -dev packages with
238 # native package binaries.
240 for impl
in sels
.selections
.values():
241 # Add mappings that have been set explicitly...
242 new_mappings
= impl
.attrs
.get(XMLNS_0COMPILE
+ ' lib-mappings', '')
244 new_mappings
= new_mappings
.split(' ')
245 for mapping
in new_mappings
:
246 assert ':' in mapping
, "lib-mappings missing ':' in '%s' from '%s'" % (mapping
, impl
.feed
)
247 name
, major_version
= mapping
.split(':', 1)
248 assert '/' not in mapping
, "lib-mappings '%s' contains a / in the version number (from '%s')!" % (mapping
, impl
.feed
)
249 if sys
.platform
== 'darwin':
250 mappings
[name
] = 'lib%s.%s.dylib' % (name
, major_version
)
252 mappings
[name
] = 'lib%s.so.%s' % (name
, major_version
)
253 # Auto-detect required mappings where possible...
254 # (if the -dev package is native, the symlinks will be OK)
255 if not is_package_impl(impl
):
256 impl_path
= lookup(impl
)
257 for libdirname
in ['lib', 'usr/lib', 'lib64', 'usr/lib64']:
258 libdir
= os
.path
.join(impl_path
, libdirname
)
259 if os
.path
.isdir(libdir
):
260 find_broken_version_symlinks(libdir
, mappings
)
263 set_up_mappings(mappings
)
265 overrides_dir
= os
.path
.join(os
.environ
['TMPDIR'], PKG_CONFIG_OVERRIDES
)
266 if os
.path
.isdir(overrides_dir
):
267 add_overrides
= model
.EnvironmentBinding('PKG_CONFIG_PATH', PKG_CONFIG_OVERRIDES
)
268 do_env_binding(add_overrides
, os
.environ
['TMPDIR'])
270 # Some programs want to put temporary build files in the source directory.
271 # Make a copy of the source if needed.
272 dup_src_type
= src_impl
.attrs
.get(XMLNS_0COMPILE
+ ' dup-src', None)
273 if dup_src_type
== 'true':
274 dup_src(shutil
.copy2
)
275 env('SRCDIR', builddir
)
277 raise Exception("Unknown dup-src value '%s'" % dup_src_type
)
280 spawn_and_check(find_in_path('sh'), [])
282 command
= src_impl
.attrs
[XMLNS_0COMPILE
+ ' command']
284 # Remove any existing log files
285 for log
in ['build.log', 'build-success.log', 'build-failure.log']:
286 if os
.path
.exists(log
):
289 # Run the command, copying output to a new log
290 log
= file('build.log', 'w')
292 print >>log
, "Build log for %s-%s" % (src_iface
.get_name(),
294 print >>log
, "\nBuilt using 0compile-%s" % __main__
.version
295 print >>log
, "\nBuild system: " + ', '.join(uname
)
296 print >>log
, "\n%s:\n" % ENV_FILE
297 shutil
.copyfileobj(file("../" + ENV_FILE
), log
)
301 if os
.path
.exists(patch_file
):
302 print >>log
, "\nPatched with:\n"
303 shutil
.copyfileobj(file(patch_file
), log
)
306 print "Executing: " + command
307 print >>log
, "Executing: " + command
309 # Tee the output to the console and to the log
310 child
= subprocess
.Popen(command
, shell
= True, stdout
= subprocess
.PIPE
, stderr
= subprocess
.STDOUT
)
312 data
= os
.read(child
.stdout
.fileno(), 100)
314 sys
.stdout
.write(data
)
316 status
= child
.wait()
319 print >>log
, "Build successful"
320 fixup_generated_pkgconfig_files()
323 failure
= "Build failed with exit code %d" % status
325 failure
= "Build failure: exited due to signal %d" % (-status
)
328 os
.rename('build.log', 'build-failure.log')
329 raise SafeException("Command '%s': %s" % (command
, failure
))
331 os
.rename('build.log', 'build-success.log')
336 """build [ --no-sandbox ] [ --shell | --force | --clean ]"""
337 buildenv
= BuildEnv()
338 sels
= buildenv
.get_selections()
340 parser
= OptionParser(usage
="usage: %prog build [options]")
342 parser
.add_option('', "--no-sandbox", help="disable use of sandboxing", action
='store_true')
343 parser
.add_option("-s", "--shell", help="run a shell instead of building", action
='store_true')
344 parser
.add_option("-c", "--clean", help="remove the build directories", action
='store_true')
345 parser
.add_option("-f", "--force", help="build even if dependencies have changed", action
='store_true')
347 parser
.disable_interspersed_args()
349 (options
, args2
) = parser
.parse_args(args
)
351 builddir
= os
.path
.realpath('build')
353 changes
= buildenv
.get_build_changes()
355 if not (options
.force
or options
.clean
):
356 raise SafeException("Build dependencies have changed:\n" +
357 '\n'.join(changes
) + "\n\n" +
358 "To build anyway, use: 0compile build --force\n" +
359 "To do a clean build: 0compile build --clean")
360 if not options
.no_sandbox
:
361 print "Build dependencies have changed:\n" + '\n'.join(changes
)
363 ensure_dir(builddir
, options
.clean
)
364 ensure_dir(buildenv
.distdir
, options
.clean
)
366 if options
.no_sandbox
:
367 return do_build_internal(options
, args2
)
369 tmpdir
= tempfile
.mkdtemp(prefix
= '0compile-')
371 my_dir
= os
.path
.dirname(__file__
)
372 readable
= ['.', my_dir
]
373 writable
= ['build', buildenv
.distdir
, tmpdir
]
374 env('TMPDIR', tmpdir
)
376 for selection
in sels
.selections
.values():
377 if not is_package_impl(selection
):
378 readable
.append(lookup(selection
))
381 if __main__
.options
.verbose
:
382 options
.append('--verbose')
384 readable
.append('/etc') # /etc/ld.*
386 spawn_maybe_sandboxed(readable
, writable
, tmpdir
, sys
.executable
, ['-u', sys
.argv
[0]] + options
+ ['build', '--no-sandbox'] + args
)
388 info("Deleting temporary directory '%s'" % tmpdir
)
389 shutil
.rmtree(tmpdir
)
391 def write_sample_interface(buildenv
, iface
, src_impl
):
392 path
= buildenv
.local_iface_file
393 target_arch
= buildenv
.target_arch
395 impl
= minidom
.getDOMImplementation()
397 XMLNS_IFACE
= namespaces
.XMLNS_IFACE
399 doc
= impl
.createDocument(XMLNS_IFACE
, "interface", None)
401 root
= doc
.documentElement
402 root
.setAttributeNS(XMLNS_NAMESPACE
, 'xmlns', XMLNS_IFACE
)
404 def addSimple(parent
, name
, text
= None):
405 elem
= doc
.createElementNS(XMLNS_IFACE
, name
)
407 parent
.appendChild(doc
.createTextNode('\n' + ' ' * (1 + depth(parent
))))
408 parent
.appendChild(elem
)
410 elem
.appendChild(doc
.createTextNode(text
))
414 element
.appendChild(doc
.createTextNode('\n' + ' ' * depth(element
)))
416 addSimple(root
, 'name', iface
.name
)
417 addSimple(root
, 'summary', iface
.summary
)
418 addSimple(root
, 'description', iface
.description
)
419 feed_for
= addSimple(root
, 'feed-for')
422 if uri
.startswith('/'):
423 print "Note: source %s is a local feed" % iface
.uri
424 for feed_uri
in iface
.feed_for
or []:
426 print "Will use <feed-for interface='%s'> instead..." % uri
429 master_feed
= minidom
.parse(uri
).documentElement
430 if master_feed
.hasAttribute('uri'):
431 uri
= master_feed
.getAttribute('uri')
432 print "Will use <feed-for interface='%s'> instead..." % uri
434 feed_for
.setAttributeNS(None, 'interface', uri
)
436 group
= addSimple(root
, 'group')
437 main
= src_impl
.attrs
.get(XMLNS_0COMPILE
+ ' binary-main', None)
439 group
.setAttributeNS(None, 'main', main
)
441 lib_mappings
= src_impl
.attrs
.get(XMLNS_0COMPILE
+ ' binary-lib-mappings', None)
443 root
.setAttributeNS(XMLNS_NAMESPACE
, 'xmlns:compile', XMLNS_0COMPILE
)
444 group
.setAttributeNS(XMLNS_0COMPILE
, 'compile:lib-mappings', lib_mappings
)
446 for d
in src_impl
.dependencies
:
447 # 0launch < 0.32 messed up the namespace...
448 if parse_bool(d
.metadata
.get('include-binary', 'false')) or \
449 parse_bool(d
.metadata
.get(XMLNS_0COMPILE
+ ' include-binary', 'false')):
450 requires
= addSimple(group
, 'requires')
451 requires
.setAttributeNS(None, 'interface', d
.interface
)
453 if isinstance(b
, model
.EnvironmentBinding
):
454 env_elem
= addSimple(requires
, 'environment')
455 env_elem
.setAttributeNS(None, 'name', b
.name
)
456 env_elem
.setAttributeNS(None, 'insert', b
.insert
)
458 env_elem
.setAttributeNS(None, 'default', b
.default
)
460 raise Exception('Unknown binding type ' + b
)
463 group
.setAttributeNS(None, 'arch', target_arch
)
464 impl_elem
= addSimple(group
, 'implementation')
465 impl_elem
.setAttributeNS(None, 'version', src_impl
.version
)
467 version_modifier
= buildenv
.version_modifier
469 impl_elem
.setAttributeNS(None, 'version-modifier', version_modifier
)
471 impl_elem
.setAttributeNS(None, 'id', '..')
472 impl_elem
.setAttributeNS(None, 'released', time
.strftime('%Y-%m-%d'))
476 stream
= codecs
.open(path
, 'w', encoding
= 'utf-8')
482 def find_broken_version_symlinks(libdir
, mappings
):
483 """libdir may be a legacy -devel package containing lib* symlinks whose
484 targets would be provided by the corresponding runtime package. If so,
485 create fixed symlinks under $TMPDIR with the real location."""
487 if sys
.platform
== 'darwin':
492 for x
in os
.listdir(libdir
):
493 if x
.startswith(prefix
) and x
.endswith(extension
):
494 path
= os
.path
.join(libdir
, x
)
495 if os
.path
.islink(path
):
496 target
= os
.readlink(path
)
497 if '/' not in target
and not os
.path
.exists(os
.path
.join(libdir
, target
)):
498 print "Broken link %s -> %s; will relocate..." % (x
, target
)
499 mappings
[x
[len(prefix
):-len(extension
)]] = target
501 def set_up_mappings(mappings
):
502 """Create a temporary directory with symlinks for each of the library mappings."""
504 if sys
.platform
== 'darwin':
505 LD_LIBRARY_PATH
='DYLD_LIBRARY_PATH'
507 LD_LIBRARY_PATH
='LD_LIBRARY_PATH'
508 for d
in os
.environ
.get(LD_LIBRARY_PATH
, '').split(':'):
509 if d
: libdirs
.append(d
)
510 libdirs
+= ['/lib', '/usr/lib']
512 def add_ldconf(config_file
):
513 if not os
.path
.isfile(config_file
):
515 for line
in file(config_file
):
517 if d
.startswith('include '):
518 glob_pattern
= d
.split(' ', 1)[1]
519 for conf
in glob
.glob(glob_pattern
):
521 elif d
and not d
.startswith('#'):
523 add_ldconf('/etc/ld.so.conf')
525 def find_library(name
, wanted
):
526 # Takes a short-name and target name of a library and returns
527 # the full path of the library.
529 path
= os
.path
.join(d
, wanted
)
530 if os
.path
.exists(path
):
532 print "WARNING: library '%s' not found (searched '%s')!" % (wanted
, libdirs
)
535 mappings_dir
= os
.path
.join(os
.environ
['TMPDIR'], 'lib-mappings')
536 os
.mkdir(mappings_dir
)
538 old_path
= os
.environ
.get('LIBRARY_PATH', '')
539 if old_path
: old_path
= ':' + old_path
540 os
.environ
['LIBRARY_PATH'] = mappings_dir
+ old_path
542 if sys
.platform
== 'darwin':
546 for name
, wanted
in mappings
.items():
547 target
= find_library(name
, wanted
)
549 print "Adding mapping lib%s%s -> %s" % (name
, soext
, target
)
550 os
.symlink(target
, os
.path
.join(mappings_dir
, 'lib' + name
+ soext
))
553 srcdir
= os
.environ
['SRCDIR'] + '/'
554 for root
, dirs
, files
in os
.walk(srcdir
):
555 assert root
.startswith(srcdir
)
556 reldir
= root
[len(srcdir
):]
558 target
= os
.path
.join(reldir
, f
)
559 #print "Copy %s -> %s" % (os.path.join(root, f), target)
560 if os
.path
.exists(target
):
562 fn(os
.path
.join(root
, f
), target
)
564 target
= os
.path
.join(reldir
, d
)
565 if not os
.path
.isdir(target
):
568 __main__
.commands
.append(do_build
)