Added experimental app support
[zeroinstall/solver.git] / zeroinstall / apps.py
blobb0b10b1e1e04c9e656adafaccbe8479958020d3c
1 """
2 Support for managing apps (as created with "0install add").
3 @since: 1.8
4 """
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
13 import re, os, time
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"""
26 if paths is None:
27 paths = os.environ['PATH'].split(os.pathsep)
28 for path in paths:
29 if path.startswith('/usr/') and not path.startswith('/usr/local/bin'):
30 # (/usr/local/bin is OK if we're running as root)
31 pass
32 elif path.startswith('/bin') or path.startswith('/sbin'):
33 pass
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
38 else:
39 break
40 else:
41 return None
43 return path
45 _command_template = """#!/bin/sh
46 exec 0install run {app} "$@"
47 """
49 class App:
50 def __init__(self, config, path):
51 self.config = config
52 self.path = path
54 def set_selections(self, sels):
55 sels_file = os.path.join(self.path, 'selections.xml')
56 dom = sels.toDOM()
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():
68 #print iface, sel
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')
76 try:
77 utime = os.stat(timestamp_path).st_mtime
78 #print "Staleness", time.time() - utime
79 need_update = False
80 except Exception as ex:
81 warn("Failed to get time-stamp of %s: %s", timestamp_path, ex)
82 need_update = True
84 # TODO: update if need_update
86 return sels
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)
91 os.close(fd)
92 os.utime(timestamp_path, None) # In case file already exists
94 def destroy(self):
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
106 os.unlink(launcher)
108 # Remove the app itself
109 import shutil
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)
126 def get_name(self):
127 return os.path.basename(self.path)
129 class AppManager:
130 def __init__(self, config):
131 self.config = config
133 def create_app(self, name):
134 validate_name(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))
139 os.mkdir(app_dir)
140 app = App(self.config, app_dir)
141 app.set_last_check()
142 return app
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):
150 if missing_ok:
151 return None
152 else:
153 raise SafeException("Invalid application name '{name}'".format(name = name))
154 app_dir = basedir.load_first_config(namespaces.config_site, "apps", name)
155 if app_dir:
156 return App(self.config, app_dir)
157 if missing_ok:
158 return None
159 else:
160 raise SafeException("No such application '{name}'".format(name = name))