1 # Copyright (C) 2006, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 """In-memory representation of the dependency graph."""
7 from zeroinstall
import SafeException
9 network_offline
= 'off-line'
10 network_minimal
= 'minimal'
12 network_levels
= (network_offline
, network_minimal
, network_full
)
14 stability_levels
= {} # Name -> Stability
16 # Default values for the 'default' attribute for <environment> bindings of
17 # well-known variables:
19 'PATH': '/bin:/usr/bin',
20 'XDG_CONFIG_DIRS': '/etc/xdg',
21 'XDG_DATA_DIRS': '/usr/local/share:/usr/share',
24 def _split_arch(arch
):
25 """Split an arch into an (os, machine) tuple. Either or both parts may be None."""
29 raise SafeException("Malformed arch '%s'" % arch
)
31 os
, machine
= arch
.split('-', 1)
32 if os
== '*': os
= None
33 if machine
== '*': machine
= None
36 def _join_arch(os
, machine
):
37 if os
== machine
== None: return None
38 return "%s-%s" % (os
or '*', machine
or '*')
40 class Stability(object):
41 __slots__
= ['level', 'name', 'description']
42 def __init__(self
, level
, name
, description
):
45 self
.description
= description
46 assert name
not in stability_levels
47 stability_levels
[name
] = self
49 def __cmp__(self
, other
):
50 return cmp(self
.level
, other
.level
)
55 insecure
= Stability(0, 'insecure', 'This is a security risk')
56 buggy
= Stability(5, 'buggy', 'Known to have serious bugs')
57 developer
= Stability(10, 'developer', 'Work-in-progress - bugs likely')
58 testing
= Stability(20, 'testing', 'Stability unknown - please test!')
59 stable
= Stability(30, 'stable', 'Tested - no serious problems found')
60 preferred
= Stability(40, 'preferred', 'Best of all - must be set manually')
62 class Restriction(object):
63 """A Restriction limits the allowed implementations of an Interface."""
65 class Binding(object):
66 """Information about how the choice of a Dependency is made known
67 to the application being run."""
69 class EnvironmentBinding(Binding
):
70 __slots__
= ['name', 'insert', 'default']
72 def __init__(self
, name
, insert
, default
= None):
75 self
.default
= default
78 return "<environ %s += %s>" % (self
.name
, self
.insert
)
81 def get_value(self
, path
, old_value
):
82 extra
= os
.path
.join(path
, self
.insert
)
84 old_value
= self
.default
or defaults
.get(self
.name
, None)
87 return extra
+ ':' + old_value
90 """An interface's feeds are other interfaces whose implementations can also be
91 used as implementations of this interface."""
92 __slots__
= ['uri', 'os', 'machine', 'user_override']
93 def __init__(self
, uri
, arch
, user_override
):
95 # This indicates whether the feed comes from the user's overrides
96 # file. If true, writer.py will write it when saving.
97 self
.user_override
= user_override
98 self
.os
, self
.machine
= _split_arch(arch
)
101 return "<Feed from %s>" % self
.uri
104 arch
= property(lambda self
: _join_arch(self
.os
, self
.machine
))
106 class Dependency(object):
107 """A Dependency indicates that an Implementation requires some additional
108 code to function, specified by another Interface."""
109 __slots__
= ['interface', 'restrictions', 'bindings']
111 def __init__(self
, interface
):
112 assert isinstance(interface
, (str, unicode))
114 self
.interface
= interface
115 self
.restrictions
= []
119 return "<Dependency on %s; bindings: %d %s>" % (self
.interface
, len(self
.bindings
), self
.bindings
)
121 class DownloadSource(object):
122 """A DownloadSource provides a way to fetch an implementation."""
123 __slots__
= ['implementation', 'url', 'size', 'extract']
125 def __init__(self
, implementation
, url
, size
, extract
):
126 assert url
.startswith('http:') or url
.startswith('ftp:') or url
.startswith('/')
127 self
.implementation
= implementation
130 self
.extract
= extract
132 class Implementation(object):
133 """An Implementation is a package which implements an Interface."""
134 __slots__
= ['os', 'machine', 'upstream_stability', 'user_stability',
135 'version', 'size', 'dependencies', 'main',
136 'id', 'download_sources', 'released', 'interface']
138 def __init__(self
, interface
, id):
139 """id can be a local path (string starting with /) or a manifest hash (eg "sha1=XXX")"""
141 self
.interface
= interface
147 self
.user_stability
= None
148 self
.upstream_stability
= None
151 self
.dependencies
= {} # URI -> Dependency
152 self
.download_sources
= [] # [DownloadSource]
154 def add_download_source(self
, url
, size
, extract
):
155 self
.download_sources
.append(DownloadSource(self
, url
, size
, extract
))
157 def get_stability(self
):
158 return self
.user_stability
or self
.upstream_stability
or testing
160 def get_version(self
):
161 return '.'.join(map(str, self
.version
))
166 def __cmp__(self
, other
):
167 """Newer versions come first"""
168 return cmp(other
.version
, self
.version
)
170 def set_arch(self
, arch
):
171 self
.os
, self
.machine
= _split_arch(arch
)
172 arch
= property(lambda self
: _join_arch(self
.os
, self
.machine
), set_arch
)
174 class Interface(object):
175 """An Interface represents some contract of behaviour."""
176 __slots__
= ['uri', 'implementations', 'name', 'description', 'summary',
177 'stability_policy', 'last_modified', 'last_local_update', 'last_checked',
178 'main', 'feeds', 'feed_for', 'metadata']
180 # last_local_update is deprecated
183 # Implementations at this level or higher are preferred.
184 # Lower levels are used only if there is no other choice.
186 def __init__(self
, uri
):
188 if uri
.startswith('http:') or uri
.startswith('/'):
192 raise SafeException("Interface name '%s' doesn't start "
193 "with 'http:'" % uri
)
196 self
.implementations
= {} # Path -> Implementation
199 self
.description
= None
200 self
.stability_policy
= None
201 self
.last_modified
= None
202 self
.last_local_update
= None
203 self
.last_checked
= None
206 self
.feed_for
= {} # URI -> True
210 return self
.name
or '(' + os
.path
.basename(self
.uri
) + ')'
213 return "<Interface %s>" % self
.uri
215 def get_impl(self
, id):
216 if id not in self
.implementations
:
217 self
.implementations
[id] = Implementation(self
, id)
218 return self
.implementations
[id]
220 def set_stability_policy(self
, new
):
221 assert new
is None or isinstance(new
, Stability
)
222 self
.stability_policy
= new
224 def get_feed(self
, uri
):
230 def add_metadata(self
, elem
):
231 self
.metadata
.append(elem
)
233 def get_metadata(self
, uri
, name
):
234 """Return a list of interface metadata elements with this name and namespace URI."""
235 return [m
for m
in self
.metadata
if m
.name
== name
and m
.uri
== uri
]
238 "Convert each %20 to a space, etc"
239 if '%' not in uri
: return uri
241 return re
.sub('%[0-9a-fA-F][0-9a-fA-F]',
242 lambda match
: chr(int(match
.group(0)[1:], 16)),
246 "Convert each space to %20, etc"
248 return re
.sub('[^-_.a-zA-Z0-9]',
249 lambda match
: '%%%02x' % ord(match
.group(0)),
252 def canonical_iface_uri(uri
):
253 if uri
.startswith('http:'):
256 iface_uri
= os
.path
.realpath(uri
)
257 if os
.path
.isfile(iface_uri
):
259 raise SafeException("Bad interface name '%s'.\n"
260 "(doesn't start with 'http:', and "
261 "doesn't exist as a local file '%s' either)" %