2 Executes a set of implementations as a program.
5 # Copyright (C) 2006, Thomas Leonard
6 # See the README file for details, or visit http://0install.net.
11 from logging
import debug
, info
13 from zeroinstall
.injector
import selections
14 from zeroinstall
.injector
.model
import SafeException
, EnvironmentBinding
, ZeroInstallImplementation
15 from zeroinstall
.injector
.iface_cache
import iface_cache
16 from zeroinstall
.support
import basedir
18 class CyclicDependencyError(Exception):
21 def do_env_binding(environ
, binding
, path
):
22 """Update this process's environment by applying the binding.
23 @param binding: the binding to apply
24 @type binding: L{model.EnvironmentBinding}
25 @param path: the selected implementation
27 environ
[binding
.name
] = binding
.get_value(path
,
28 environ
.get(binding
.name
, None))
29 info("%s=%s", binding
.name
, environ
[binding
.name
])
31 def add_env_bindings(environ
, sels
):
32 for selection
in sels
.selections
.values():
33 _do_bindings(sels
, environ
, selection
, selection
.bindings
)
34 for dep
in selection
.dependencies
:
35 dep_impl
= sels
.selections
[dep
.interface
]
36 if not dep_impl
.id.startswith('package:'):
37 _do_bindings(sels
, environ
, dep_impl
, dep
.bindings
)
39 debug("Implementation %s is native; no bindings needed", dep_impl
)
41 def execute(policy
, prog_args
, dry_run
= False, main
= None, wrapper
= None):
42 """Execute program. On success, doesn't return. On failure, raises an Exception.
43 Returns normally only for a successful dry run.
44 @param policy: a policy with the selected versions
45 @type policy: L{policy.Policy}
46 @param prog_args: arguments to pass to the program
47 @type prog_args: [str]
48 @param dry_run: if True, just print a message about what would have happened
50 @param main: the name of the binary to run, or None to use the default
52 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
54 @precondition: C{policy.ready and policy.get_uncached_implementations() == []}
56 selns
= selections
.Selections(policy
)
57 execute_selections(selns
, prog_args
, dry_run
=dry_run
, main
=main
,
60 def _do_bindings(selns
, environ
, impl
, bindings
):
62 if isinstance(b
, EnvironmentBinding
):
63 do_env_binding(environ
, b
, _get_implementation_path(selns
, impl
))
65 def read_file(filename
):
66 fh
= open(filename
, "r")
72 def write_file(filename
, data
):
73 fh
= open(filename
, "w")
79 def copy_tree(src_path
, dest_path
, copy_file
):
80 if os
.path
.islink(src_path
):
81 if os
.path
.islink(dest_path
):
83 os
.symlink(os
.readlink(src_path
), dest_path
)
84 elif os
.path
.isdir(src_path
):
85 if not os
.path
.exists(dest_path
):
87 for leafname
in os
.listdir(src_path
):
88 copy_tree(os
.path
.join(src_path
, leafname
),
89 os
.path
.join(dest_path
, leafname
),
92 copy_file(src_path
, dest_path
)
94 def rewrite_dir(impl_dir
, dep_rewrites
, self_rewrites
):
95 cache_dir
= os
.path
.join(basedir
.xdg_cache_home
, "0install.net", "rewrites")
96 if not os
.path
.exists(cache_dir
):
97 os
.makedirs(cache_dir
)
98 digest
= hashlib
.sha1(str(dep_rewrites
+ self_rewrites
)).hexdigest()
99 # We don't need the full path. Rewrites are local.
100 leafname
= digest
[:8]
101 dest_dir
= os
.path
.join(cache_dir
, leafname
)
102 # TODO: fix race condition
103 if os
.path
.exists(dest_dir
):
105 rewrites
= dep_rewrites
[:]
106 for rewrite
in self_rewrites
:
107 dest_dir2
= _make_fixed_length_link(dest_dir
, len(rewrite
))
108 rewrites
.append((rewrite
, dest_dir2
))
110 def copy_file(src_path
, dest_path
):
111 data
= read_file(src_path
)
112 # This is not an optimal way to do several
113 # substitutions, but when I tried to use the "array"
114 # module to do it in-place, it was slower.
115 for from_str
, to_str
in rewrites
:
116 data
= data
.replace(from_str
, to_str
)
117 write_file(dest_path
, data
)
118 os
.chmod(dest_path
, os
.stat(src_path
).st_mode
)
120 copy_tree(impl_dir
, dest_dir
, copy_file
)
123 def _make_fixed_length_link(pathname
, length
):
124 link_dir
= os
.path
.join(basedir
.xdg_cache_home
, "0install.net", "ln")
125 if not os
.path
.exists(link_dir
):
126 os
.makedirs(link_dir
)
127 digest
= hashlib
.sha1(pathname
).hexdigest()[:8]
128 dest_base
= os
.path
.join(link_dir
, digest
)
129 assert len(dest_base
) <= length
, (dest_base
, len(dest_base
), length
)
130 dest_path
= dest_base
+ "X" * (length
- len(dest_base
))
132 os
.symlink(pathname
, dest_path
)
134 if exn
.errno
!= errno
.EEXIST
:
138 CYCLIC_REF
= object()
141 # TODO: use weak references?
147 cache
[args
] = CYCLIC_REF
151 if value
is CYCLIC_REF
:
152 raise CyclicDependencyError()
157 def _get_implementation_path(selns
, impl
):
158 if impl
.id.startswith('/'):
161 for dependency
in impl
.dependencies
:
162 for rewrite_string
in dependency
.rewrites
:
163 dep_impl
= selns
.selections
[dependency
.interface
]
164 to_path
= _get_implementation_path(selns
, dep_impl
)
165 to_path
= _make_fixed_length_link(to_path
, len(rewrite_string
))
166 rewrites
.append((rewrite_string
, to_path
))
167 impl_dir
= iface_cache
.stores
.lookup(impl
.id)
168 if len(rewrites
) == 0 and len(impl
.self_rewrites
) == 0:
170 return rewrite_dir(impl_dir
, rewrites
, impl
.self_rewrites
)
172 def execute_selections(selections
, prog_args
, dry_run
= False, main
= None, wrapper
= None):
173 """Execute program. On success, doesn't return. On failure, raises an Exception.
174 Returns normally only for a successful dry run.
175 @param selections: the selected versions
176 @type selections: L{selections.Selections}
177 @param prog_args: arguments to pass to the program
178 @type prog_args: [str]
179 @param dry_run: if True, just print a message about what would have happened
181 @param main: the name of the binary to run, or None to use the default
183 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
186 @precondition: All implementations are in the cache.
188 add_env_bindings(os
.environ
, selections
)
189 _execute(selections
, prog_args
, dry_run
, main
, wrapper
)
191 def test_selections(selections
, prog_args
, dry_run
, main
, wrapper
= None):
192 """Run the program in a child process, collecting stdout and stderr.
193 @return: the output produced by the process
198 output
= tempfile
.TemporaryFile(prefix
= '0launch-test')
205 os
.dup2(output
.fileno(), 1)
206 os
.dup2(output
.fileno(), 2)
207 execute_selections(selections
, prog_args
, dry_run
, main
)
210 traceback
.print_exc()
216 info("Waiting for test process to finish...")
218 pid
, status
= os
.waitpid(child
, 0)
222 results
= output
.read()
224 results
+= "Error from child process: exit code = %d" % status
230 def get_executable_for_selections(selns
, main
=None):
231 root_impl
= selns
.selections
[selns
.interface
]
232 assert root_impl
is not None
234 if root_impl
.id.startswith('package:'):
235 main
= main
or root_impl
.main
239 main
= root_impl
.main
240 elif main
.startswith('/'):
243 main
= os
.path
.join(os
.path
.dirname(root_impl
.main
), main
)
245 prog_path
= os
.path
.join(_get_implementation_path(selns
, root_impl
), main
)
248 raise SafeException("Implementation '%s' cannot be executed directly; it is just a library "
249 "to be used by other programs (or missing 'main' attribute)" %
252 if not os
.path
.exists(prog_path
):
253 raise SafeException("File '%s' does not exist.\n"
254 "(implementation '%s' + program '%s')" %
255 (prog_path
, root_impl
.id, main
))
258 def _execute(selns
, prog_args
, dry_run
, main
, wrapper
):
259 prog_path
= get_executable_for_selections(selns
, main
)
261 prog_args
= ['-c', wrapper
+ ' "$@"', '-', prog_path
] + list(prog_args
)
262 prog_path
= '/bin/sh'
265 print "Would execute:", prog_path
, ' '.join(prog_args
)
267 info("Executing: %s", prog_path
)
271 os
.execl(prog_path
, prog_path
, *prog_args
)
273 raise SafeException("Failed to run '%s': %s" % (prog_path
, str(ex
)))