2 Support for managing apps (as created with "0install add").
6 # Copyright (C) 2012, Thomas Leonard
7 # See the README file for details, or visit http://0install.net.
9 from zeroinstall
import _
, SafeException
10 from zeroinstall
.support
import basedir
11 from zeroinstall
.injector
import namespaces
, selections
, qdom
12 from logging
import warn
15 # Avoid characters that are likely to cause problems (reject : and ; everywhere
16 # so that apps can be portable between POSIX and Windows).
17 valid_name
= re
.compile(r
'''^[^./\\:=;'"][^/\\:=;'"]*$''')
19 def validate_name(name
):
20 if valid_name
.match(name
): return
21 raise SafeException("Invalid application name '{name}'".format(name
= name
))
23 def find_bin_dir(paths
= None):
24 """Find the first writable path in the list (default $PATH),
25 skipping /bin, /sbin and everything under /usr except /usr/local/bin"""
27 paths
= os
.environ
['PATH'].split(os
.pathsep
)
29 if path
.startswith('/usr/') and not path
.startswith('/usr/local/bin'):
30 # (/usr/local/bin is OK if we're running as root)
32 elif path
.startswith('/bin') or path
.startswith('/sbin'):
34 elif os
.path
.realpath(path
).startswith(basedir
.xdg_cache_home
):
35 pass # print "Skipping cache", first_path
36 elif not os
.access(path
, os
.W_OK
):
37 pass # print "No access", first_path
45 _command_template
= """#!/bin/sh
46 exec 0install run {app} "$@"
50 def __init__(self
, config
, path
):
54 def set_selections(self
, sels
):
55 sels_file
= os
.path
.join(self
.path
, 'selections.xml')
57 with
open(sels_file
, 'w') as stream
:
58 dom
.writexml(stream
, addindent
=" ", newl
="\n", encoding
= 'utf-8')
60 def get_selections(self
):
61 sels_file
= os
.path
.join(self
.path
, 'selections.xml')
62 with
open(sels_file
) as stream
:
63 sels
= selections
.Selections(qdom
.parse(stream
))
65 stores
= self
.config
.stores
67 for iface
, sel
in sels
.selections
.iteritems():
69 if sel
.id.startswith('package:'):
70 pass # TODO: check version is the same
71 elif not sel
.is_available(stores
):
72 print "missing", sel
# TODO: download
74 # Check the selections are still available and up-to-date
75 timestamp_path
= os
.path
.join(self
.path
, 'last-check')
77 utime
= os
.stat(timestamp_path
).st_mtime
78 #print "Staleness", time.time() - utime
80 except Exception as ex
:
81 warn("Failed to get time-stamp of %s: %s", timestamp_path
, ex
)
84 # TODO: update if need_update
88 def set_last_check(self
):
89 timestamp_path
= os
.path
.join(self
.path
, 'last-check')
90 fd
= os
.open(timestamp_path
, os
.O_WRONLY | os
.O_CREAT
, 0o644)
92 os
.utime(timestamp_path
, None) # In case file already exists
95 # Check for shell command
96 # TODO: remember which commands we own instead of guessing
97 name
= self
.get_name()
98 bin_dir
= find_bin_dir()
99 launcher
= os
.path
.join(bin_dir
, name
)
100 expanded_template
= _command_template
.format(app
= name
)
101 if os
.path
.exists(launcher
) and os
.path
.getsize(launcher
) == len(expanded_template
):
102 with
open(launcher
, 'r') as stream
:
103 contents
= stream
.read()
104 if contents
== expanded_template
:
105 #print "rm", launcher
108 # Remove the app itself
110 shutil
.rmtree(self
.path
)
112 def integrate_shell(self
, name
):
113 # TODO: remember which commands we create
114 if not valid_name
.match(name
):
115 raise SafeException("Invalid shell command name '{name}'".format(name
= name
))
116 bin_dir
= find_bin_dir()
117 launcher
= os
.path
.join(bin_dir
, name
)
118 if os
.path
.exists(launcher
):
119 raise SafeException("Command already exists: {path}".format(path
= launcher
))
121 with
open(launcher
, 'w') as stream
:
122 stream
.write(_command_template
.format(app
= self
.get_name()))
123 # Make new script executable
124 os
.chmod(launcher
, 0o111 | os
.fstat(stream
.fileno()).st_mode
)
127 return os
.path
.basename(self
.path
)
130 def __init__(self
, config
):
133 def create_app(self
, name
):
135 apps_dir
= basedir
.save_config_path(namespaces
.config_site
, "apps")
136 app_dir
= os
.path
.join(apps_dir
, name
)
137 if os
.path
.isdir(app_dir
):
138 raise SafeException(_("Application '{name}' already exists: {path}").format(name
= name
, path
= app_dir
))
140 app
= App(self
.config
, app_dir
)
144 def lookup_app(self
, name
, missing_ok
= False):
145 """Get the App for name.
146 Returns None if name is not an application (doesn't exist or is not a valid name).
147 Since / and : are not valid name characters, it is generally safe to try this
148 before calling L{model.canonical_iface_uri}."""
149 if not valid_name
.match(name
):
153 raise SafeException("Invalid application name '{name}'".format(name
= name
))
154 app_dir
= basedir
.load_first_config(namespaces
.config_site
, "apps", name
)
156 return App(self
.config
, app_dir
)
160 raise SafeException("No such application '{name}'".format(name
= name
))