1 """
2 Chooses a set of components to make a running program.
3 """
4
5
6
7
8 from zeroinstall import _
9 import locale
10 from logging import debug, warn, info
11
12 from zeroinstall.injector.reader import MissingLocalFeed
13 from zeroinstall.injector.arch import machine_groups
14 from zeroinstall.injector import model, sat, selections
17 - def __init__(self, name, command, impl, arch):
22
26
28 is_dummy = False
29
30 - def __init__(self, iface, impl, arch, dummy = False):
31 self.iface = iface
32 self.impl = impl
33 self.arch = arch
34 if dummy:
35 self.is_dummy = True
36
40
57
59 """Chooses a set of implementations to satisfy the requirements of a program and its user.
60 Typical use:
61 1. Create a Solver object and configure it
62 2. Call L{solve}.
63 3. If any of the returned feeds_used are stale or missing, you may like to start downloading them
64 4. If it is 'ready' then you can download and run the chosen versions.
65 @ivar selections: the chosen implementation of each interface
66 @type selections: L{selections.Selections}
67 @ivar requires: the selected dependencies for each chosen version
68 @type requires: {L{model.Interface}: [L{model.Dependency}]}
69 @ivar feeds_used: the feeds which contributed to the choice in L{selections}
70 @type feeds_used: set(str)
71 @ivar record_details: whether to record information about unselected implementations
72 @type record_details: {L{Interface}: [(L{Implementation}, str)]}
73 @ivar details: extra information, if record_details mode was used
74 @type details: {str: [(Implementation, comment)]}
75 """
76 __slots__ = ['selections', 'requires', 'feeds_used', 'details', 'record_details', 'ready']
77
79 self.selections = self.requires = self.feeds_used = self.details = None
80 self.record_details = False
81 self.ready = False
82
83 - def solve(self, root_interface, root_arch, command_name = 'run'):
84 """Get the best implementation of root_interface and all of its dependencies.
85 @param root_interface: the URI of the program to be solved
86 @type root_interface: str
87 @param root_arch: the desired target architecture
88 @type root_arch: L{arch.Architecture}
89 @param command_name: which <command> element to select
90 @type command_name: str | None
91 @postcondition: self.ready, self.selections and self.feeds_used are updated"""
92 raise NotImplementedError("Abstract")
93
95 """Converts the problem to a set of pseudo-boolean constraints and uses a PB solver to solve them.
96 @ivar langs: the preferred languages (e.g. ["es_ES", "en"]). Initialised to the current locale.
97 @type langs: str"""
98
99 __slots__ = ['_failure_reason', 'config', 'extra_restrictions', '_lang_ranks', '_langs']
100
101 @property
104
105 - def __init__(self, config, extra_restrictions = None):
106 """
107 @param config: policy preferences (e.g. stability), the iface_cache and the stores to use
108 @type config: L{policy.Config}
109 @param extra_restrictions: extra restrictions on the chosen implementations
110 @type extra_restrictions: {L{model.Interface}: [L{model.Restriction}]}
111 """
112 Solver.__init__(self)
113 assert not isinstance(config, str), "API change!"
114 self.config = config
115 self.extra_restrictions = extra_restrictions or {}
116
117
118 self.langs = [locale.getlocale()[0] or 'en', 'en']
119
121 """Set the preferred languages.
122 @param langs: languages (and regions), first choice first
123 @type langs: [str]
124 """
125
126 _lang_ranks = {}
127 score = 0
128 i = len(langs)
129
130 while i > 0:
131 i -= 1
132 lang = langs[i].replace('_', '-')
133 _lang_ranks[lang.split('-')[0]] = score
134 _lang_ranks[lang] = score + 1
135 score += 2
136 self._langs = langs
137 self._lang_ranks = _lang_ranks
138
139 langs = property(lambda self: self._langs, set_langs)
140
142 impl_langs = (impl.langs or 'en').split()
143 my_langs = self._lang_ranks
144
145 stores = self.config.stores
146 is_available = impl.is_available(stores)
147
148
149 stab_policy = interface.stability_policy
150 if not stab_policy:
151 if self.config.help_with_testing: stab_policy = model.testing
152 else: stab_policy = model.stable
153
154 stability = impl.get_stability()
155 if stability >= stab_policy:
156 stability_limited = model.preferred
157 else:
158 stability_limited = stability
159
160 return [
161
162 max(my_langs.get(l.split('-')[0], -1) for l in impl_langs),
163
164
165 stability == model.preferred,
166
167
168 self.config.network_use != model.network_full and is_available,
169
170
171 not impl.requires_root_install,
172
173
174
175
176 stability_limited,
177
178
179 impl.version[0],
180
181
182 impl.id.startswith('package:'),
183
184
185
186 impl.version,
187
188
189 -arch.os_ranks.get(impl.os, 999),
190
191
192 -arch.machine_ranks.get(impl.machine, 999),
193
194
195
196 max(my_langs.get(l, -1) for l in impl_langs),
197
198
199 is_available,
200
201
202 impl.id
203 ]
204
205 - def solve(self, root_interface, root_arch, command_name = 'run', closest_match = False):
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227 iface_cache = self.config.iface_cache
228
229 problem = sat.SATProblem()
230
231 impl_to_var = {}
232 self.feeds_used = set()
233 self.requires = {}
234 self.ready = False
235 self.details = self.record_details and {}
236
237 self.selections = None
238 self._failure_reason = None
239
240 ifaces_processed = set()
241
242 impls_for_machine_group = {0 : []}
243 for machine_group in machine_groups.values():
244 impls_for_machine_group[machine_group] = []
245
246 impls_for_iface = {}
247
248 group_clause_for = {}
249 group_clause_for_command = {}
250
251
252
253
254 def deps_in_use(impl, arch):
255 for dep in impl.requires:
256 use = dep.metadata.get("use", None)
257 if use not in arch.use:
258 continue
259 yield dep
260
261
262
263
264
265
266
267
268 def find_dependency_candidates(requiring_impl_var, dependency):
269 def meets_restrictions(candidate):
270 for r in dependency.restrictions:
271 if not r.meets_restriction(candidate):
272
273 return False
274 return True
275
276 essential = dependency.importance == model.Dependency.Essential
277
278 dep_iface = iface_cache.get_interface(dependency.interface)
279 dep_union = [sat.neg(requiring_impl_var)]
280 for candidate in impls_for_iface[dep_iface]:
281 if (candidate.__class__ is _DummyImpl) or meets_restrictions(candidate):
282 if essential:
283 c_var = impl_to_var.get(candidate, None)
284 if c_var is not None:
285 dep_union.append(c_var)
286
287 else:
288
289
290
291
292
293 if not essential:
294 c_var = impl_to_var.get(candidate, None)
295 if c_var is not None:
296 problem.add_clause(dep_union + [sat.neg(c_var)])
297
298
299 if essential:
300 problem.add_clause(dep_union)
301
302 def is_unusable(impl, restrictions, arch):
303 """@return: whether this implementation is unusable.
304 @rtype: bool"""
305 return get_unusable_reason(impl, restrictions, arch) != None
306
307 def get_unusable_reason(impl, restrictions, arch):
308 """
309 @param impl: Implementation to test.
310 @type restrictions: [L{model.Restriction}]
311 @return: The reason why this impl is unusable, or None if it's OK.
312 @rtype: str
313 @note: The restrictions are for the interface being requested, not the feed
314 of the implementation; they may be different when feeds are being used."""
315 for r in restrictions:
316 if not r.meets_restriction(impl):
317 return _("Incompatible with another selected implementation")
318 stability = impl.get_stability()
319 if stability <= model.buggy:
320 return stability.name
321 if (self.config.network_use == model.network_offline or not impl.download_sources) and not impl.is_available(self.config.stores):
322 if not impl.download_sources:
323 return _("No retrieval methods")
324 return _("Not cached and we are off-line")
325 if impl.os not in arch.os_ranks:
326 return _("Unsupported OS")
327 if impl.machine not in arch.machine_ranks:
328 if impl.machine == 'src':
329 return _("Source code")
330 return _("Unsupported machine type")
331 return None
332
333 def usable_feeds(iface, arch):
334 """Return all feeds for iface that support arch.
335 @rtype: generator(ZeroInstallFeed)"""
336 yield iface.uri
337
338 for f in iface_cache.get_feed_imports(iface):
339
340 if f.os in arch.os_ranks and \
341 (f.machine is None or f.machine in arch.machine_ranks):
342 yield f.uri
343 else:
344 debug(_("Skipping '%(feed)s'; unsupported architecture %(os)s-%(machine)s"),
345 {'feed': f, 'os': f.os, 'machine': f.machine})
346
347
348
349 def process_dependencies(requiring_var, requirer, arch):
350 for d in deps_in_use(requirer, arch):
351 debug(_("Considering command dependency %s"), d)
352
353 add_iface(d.interface, arch.child_arch)
354
355 for c in d.get_required_commands():
356
357 command_vars = add_command_iface(d.interface, arch.child_arch, c)
358
359
360
361 problem.add_clause([sat.neg(requiring_var)] + command_vars)
362
363
364 find_dependency_candidates(requiring_var, d)
365
366 def add_iface(uri, arch):
367 """Name implementations from feed and assert that only one can be selected."""
368 if uri in ifaces_processed: return
369 ifaces_processed.add(uri)
370
371 iface = iface_cache.get_interface(uri)
372
373 impls = []
374 for f in usable_feeds(iface, arch):
375 self.feeds_used.add(f)
376 debug(_("Processing feed %s"), f)
377
378 try:
379 feed = iface_cache.get_feed(f)
380 if feed is None: continue
381
382
383
384 if feed.implementations:
385 impls.extend(feed.implementations.values())
386
387 distro_feed_url = feed.get_distro_feed()
388 if distro_feed_url:
389 self.feeds_used.add(distro_feed_url)
390 distro_feed = iface_cache.get_feed(distro_feed_url)
391 if distro_feed.implementations:
392 impls.extend(distro_feed.implementations.values())
393 except MissingLocalFeed as ex:
394 warn(_("Missing local feed; if it's no longer required, remove it with:") +
395 '\n0install remove-feed ' + iface.uri + ' ' + f,
396 {'feed': f, 'interface': iface, 'exception': ex})
397 except Exception as ex:
398 warn(_("Failed to load feed %(feed)s for %(interface)s: %(exception)s"), {'feed': f, 'interface': iface, 'exception': ex})
399
400
401 impls.sort(key = lambda impl: self.get_rating(iface, impl, arch), reverse = True)
402
403 impls_for_iface[iface] = filtered_impls = []
404
405 my_extra_restrictions = self.extra_restrictions.get(iface, [])
406
407 if self.record_details:
408 self.details[iface] = [(impl, get_unusable_reason(impl, my_extra_restrictions, arch)) for impl in impls]
409
410 var_names = []
411 for impl in impls:
412 if is_unusable(impl, my_extra_restrictions, arch):
413 continue
414
415 filtered_impls.append(impl)
416
417 assert impl not in impl_to_var
418 v = problem.add_variable(ImplInfo(iface, impl, arch))
419 impl_to_var[impl] = v
420 var_names.append(v)
421
422 if impl.machine and impl.machine != 'src':
423 impls_for_machine_group[machine_groups.get(impl.machine, 0)].append(v)
424
425 process_dependencies(v, impl, arch)
426
427 if closest_match:
428 dummy_impl = _DummyImpl()
429 dummy_var = problem.add_variable(ImplInfo(iface, dummy_impl, arch, dummy = True))
430 var_names.append(dummy_var)
431 impl_to_var[dummy_impl] = dummy_var
432 filtered_impls.append(dummy_impl)
433
434
435 if uri == root_interface:
436 if var_names:
437 clause = problem.at_most_one(var_names)
438 problem.add_clause(var_names)
439 else:
440 problem.impossible()
441 clause = False
442 elif var_names:
443 clause = problem.at_most_one(var_names)
444 else:
445
446
447 return
448
449 assert clause is not True
450 assert clause is not None
451 if clause is not False:
452 group_clause_for[uri] = clause
453
454 def add_command_iface(uri, arch, command_name):
455 """Add every <command> in interface 'uri' with this name.
456 Each one depends on the corresponding implementation and only
457 one can be selected. If closest_match is on, include a dummy
458 command that can always be selected."""
459
460
461 existing = group_clause_for_command.get((uri, command_name), None)
462 if existing is not None:
463 return existing.lits
464
465
466
467
468 add_iface(uri, arch)
469
470 iface = iface_cache.get_interface(uri)
471 filtered_impls = impls_for_iface[iface]
472
473 var_names = []
474 for impl in filtered_impls:
475 command = impl.commands.get(command_name, None)
476 if not command:
477 if not isinstance(impl, _DummyImpl):
478
479 problem.add_clause([sat.neg(impl_to_var[impl])])
480 continue
481
482
483
484 command_var = problem.add_variable(CommandInfo(command_name, command, impl, arch))
485 problem.add_clause([impl_to_var[impl], sat.neg(command_var)])
486
487 var_names.append(command_var)
488
489 process_dependencies(command_var, command, arch)
490
491
492 if self.record_details:
493 def new_reason(impl, old_reason):
494 if command_name in impl.commands:
495 return old_reason
496 return old_reason or (_('No %s command') % command_name)
497 self.details[iface] = [(impl, new_reason(impl, reason)) for (impl, reason) in self.details[iface]]
498
499 if closest_match:
500 dummy_command = problem.add_variable(None)
501 var_names.append(dummy_command)
502
503 if var_names:
504
505 assert (uri, command_name) not in group_clause_for_command
506 group_clause_for_command[(uri, command_name)] = problem.at_most_one(var_names)
507
508 return var_names
509
510 if command_name is None:
511 add_iface(root_interface, root_arch)
512 else:
513 commands = add_command_iface(root_interface, root_arch, command_name)
514 if len(commands) > int(closest_match):
515
516 problem.add_clause(commands)
517 else:
518
519 info("No %s <command> in %s", command_name, root_interface)
520
521 impls = impls_for_iface[iface_cache.get_interface(root_interface)]
522 if impls == [] or (len(impls) == 1 and isinstance(impls[0], _DummyImpl)):
523
524 if self.config.network_use == model.network_offline:
525 self._failure_reason = _("Interface '%s' has no usable implementations in the cache (and 0install is in off-line mode)") % root_interface
526 else:
527 self._failure_reason = _("Interface '%s' has no usable implementations") % root_interface
528 else:
529
530 self._failure_reason = _("Interface '%s' cannot be executed directly; it is just a library "
531 "to be used by other programs (or missing '%s' command)") % (root_interface, command_name)
532
533 problem.impossible()
534
535
536 m_groups = []
537 for machine_group, impls in impls_for_machine_group.iteritems():
538 m_group = 'm%d' % machine_group
539 group_var = problem.add_variable(m_group)
540 if impls:
541 for impl in impls:
542 problem.add_clause([group_var, sat.neg(impl)])
543 m_groups.append(group_var)
544 if m_groups:
545 m_groups_clause = problem.at_most_one(m_groups)
546 else:
547 m_groups_clause = None
548
549 def decide():
550 """Recurse through the current selections until we get to an interface with
551 no chosen version, then tell the solver to try the best version from that."""
552
553 def find_undecided_dep(impl_or_command, arch):
554
555 for dep in deps_in_use(impl_or_command, arch):
556 for c in dep.get_required_commands():
557 dep_lit = find_undecided_command(dep.interface, c)
558 if dep_lit is not None:
559 return dep_lit
560 dep_lit = find_undecided(dep.interface)
561 if dep_lit is not None:
562 return dep_lit
563 return None
564
565 seen = set()
566 def find_undecided(uri):
567 if uri in seen:
568 return
569 seen.add(uri)
570
571 group = group_clause_for.get(uri, None)
572
573 if group is None:
574
575
576 return
577
578
579 lit = group.current
580 if lit is None:
581 return group.best_undecided()
582
583
584
585 lit_info = problem.get_varinfo_for_lit(lit).obj
586 return find_undecided_dep(lit_info.impl, lit_info.arch)
587
588 def find_undecided_command(uri, name):
589 if name is None: return find_undecided(uri)
590
591 group = group_clause_for_command[(uri, name)]
592 lit = group.current
593 if lit is None:
594 return group.best_undecided()
595
596
597
598
599 lit_info = problem.get_varinfo_for_lit(lit).obj
600 if lit_info is None:
601 assert closest_match
602 return None
603 return find_undecided_dep(lit_info.command, lit_info.arch) or \
604 find_undecided_dep(lit_info.impl, lit_info.arch)
605
606 best = find_undecided_command(root_interface, command_name)
607 if best is not None:
608 return best
609
610
611
612 for group in group_clause_for.values() + group_clause_for_command.values() + [m_groups_clause]:
613 if group.current is None:
614 best = group.best_undecided()
615 if best is not None:
616 return sat.neg(best)
617
618 return None
619
620 ready = problem.run_solver(decide) is True
621
622 if not ready and not closest_match:
623
624
625
626 self.solve(root_interface, root_arch, command_name = command_name, closest_match = True)
627 else:
628 self.ready = ready and not closest_match
629 self.selections = selections.Selections(None)
630 self.selections.interface = root_interface
631
632 sels = self.selections.selections
633
634 commands_needed = []
635
636
637
638 for uri, group in group_clause_for.iteritems():
639 if group.current is not None:
640 lit_info = problem.get_varinfo_for_lit(group.current).obj
641 if lit_info.is_dummy:
642 sels[lit_info.iface.uri] = None
643 else:
644
645 impl = lit_info.impl
646
647 for b in impl.bindings:
648 c = b.command
649 if c is not None:
650 commands.append((uri, c))
651
652 deps = self.requires[lit_info.iface] = []
653 for dep in deps_in_use(lit_info.impl, lit_info.arch):
654 deps.append(dep)
655 for c in dep.get_required_commands():
656 commands_needed.append((dep.interface, c))
657
658 sels[lit_info.iface.uri] = selections.ImplSelection(lit_info.iface.uri, impl, deps)
659
660
661 def add_command(iface, name):
662 sel = sels.get(iface, None)
663 if sel:
664 command = sel.impl.commands[name]
665 if name in sel._used_commands:
666 return
667 sel._used_commands.add(name)
668
669 for dep in command.requires:
670 for dep_command_name in dep.get_required_commands():
671 add_command(dep.interface, dep_command_name)
672
673
674
675 for b in command.bindings:
676 c = b.command
677 if c is not None:
678 add_command(iface, c)
679
680 for iface, command in commands_needed:
681 add_command(iface, command)
682
683 if command_name is not None:
684 self.selections.command = command_name
685 add_command(root_interface, command_name)
686
688 """Return an exception explaining why the solve failed."""
689 assert not self.ready
690
691 if self._failure_reason:
692 return model.SafeException(self._failure_reason)
693
694 return model.SafeException(_("Can't find all required implementations:") + '\n' +
695 '\n'.join(["- %s -> %s" % (iface, self.selections[iface])
696 for iface in self.selections]))
697
698 DefaultSolver = SATSolver
699