-
-
Notifications
You must be signed in to change notification settings - Fork 405
/
uninstall.py
327 lines (257 loc) · 11.5 KB
/
uninstall.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
"""
Command-line script for uninstalling an existing Sage spkg from $SAGE_LOCAL.
This performs two types of uninstallation:
1) Old-style uninstallation: This is close to what existed before this
script, where *some* packages had uninstall steps (mostly consisting of
some broad `rm -rf` commands) that were run before installing new
versions of the packages. This convention was applied inconsistently,
but for those packages that did have old-style uninstall steps, those
steps should be in a script called `spkg-legacy-uninstall` under the
spkg directory (build/pkgs/<pkg_name>). If this script is found, it is
run for backwards-compatibility support.
2) New-style uninstallation: More recently installed packages that were
installed with staged installation have a record of all files installed
by that package. That file is stored in the $SAGE_SPKG_INST directory
(typically $SAGE_LOCAL/var/lib/sage/installed) and is created when the
spkg is installed. This is a JSON file containing some meta-data about
the package, including the list of all files associated with the
package. This script removes all these files, including the record
file. Any directories that are empty after files are removed from them
are also removed.
"""
#*****************************************************************************
# Copyright (C) 2017 Erik M. Bray <erik.m.bray@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
# http://www.gnu.org/licenses/
#*****************************************************************************
from __future__ import print_function
import glob
import json
import os
import shutil
import subprocess
import sys
from os import path as pth
try:
import argparse
except ImportError:
from sage_bootstrap.compat import argparse
from .env import SAGE_ROOT
PKGS = pth.join(SAGE_ROOT, 'build', 'pkgs')
"""Directory where all spkg sources are found."""
def uninstall(spkg_name, sage_local, keep_files=False, verbose=False):
"""
Given a package name and path to SAGE_LOCAL, uninstall that package from
SAGE_LOCAL if it is currently installed.
"""
# The default path to this directory; however its value should be read
# from the environment if possible
spkg_inst = pth.join(sage_local, 'var', 'lib', 'sage', 'installed')
spkg_inst = os.environ.get('SAGE_SPKG_INST', spkg_inst)
# Find all stamp files for the package; there should be only one, but if
# there is somehow more than one we'll work with the most recent and delete
# the rest
pattern = pth.join(spkg_inst, '{0}-*'.format(spkg_name))
stamp_files = sorted(glob.glob(pattern), key=pth.getmtime)
if keep_files:
print("Removing stamp file but keeping package files")
remove_stamp_files(stamp_files)
return
if stamp_files:
stamp_file = stamp_files[-1]
else:
stamp_file = None
spkg_meta = {}
if stamp_file:
try:
with open(stamp_file) as f:
spkg_meta = json.load(f)
except (OSError, ValueError):
pass
if 'files' not in spkg_meta:
if stamp_file:
print("Old-style or corrupt stamp file '{0}'"
.format(stamp_file), file=sys.stderr)
else:
print("Package '{0}' is currently not installed"
.format(spkg_name), file=sys.stderr)
# Run legacy uninstaller even if there is no stamp file: the
# package may be partially installed without a stamp file
legacy_uninstall(spkg_name, verbose=verbose)
else:
files = spkg_meta['files']
if not files:
print("Warning: No files to uninstall for "
"'{0}'".format(spkg_name), file=sys.stderr)
modern_uninstall(spkg_name, sage_local, files, verbose=verbose)
remove_stamp_files(stamp_files, verbose=verbose)
def legacy_uninstall(spkg_name, verbose=False):
"""
Run the spkg's legacy uninstall script, if one exists; otherwise do
nothing.
"""
spkg_dir = pth.join(PKGS, spkg_name)
legacy_uninstall = pth.join(spkg_dir, 'spkg-legacy-uninstall')
if not pth.isfile(legacy_uninstall):
print("No legacy uninstaller found for '{0}'; nothing to "
"do".format(spkg_name), file=sys.stderr)
return
print("Uninstalling '{0}' with legacy uninstaller".format(spkg_name))
if verbose:
with open(legacy_uninstall) as f:
print(f.read())
# Any errors from this, including a non-zero return code will
# bubble up and exit the uninstaller
subprocess.check_call(['bash', legacy_uninstall])
def modern_uninstall(spkg_name, sage_local, files, verbose=False):
"""
Remove all listed files from the given SAGE_LOCAL (all file paths should
be assumed relative to SAGE_LOCAL).
This is otherwise (currently) agnostic about what package is actually
being uninstalled--all it cares about is removing a list of files.
If the directory containing any of the listed files is empty after all
files are removed then the directory is removed as well.
"""
spkg_scripts = pth.join(sage_local, 'var', 'lib', 'sage', 'scripts')
spkg_scripts = os.environ.get('SAGE_SPKG_SCRIPTS', spkg_scripts)
spkg_scripts = pth.join(spkg_scripts, spkg_name)
# Sort the given files first by the highest directory depth, then by name,
# so that we can easily remove a directory once it's been emptied
files.sort(key=lambda f: (-f.count(os.sep), f))
print("Uninstalling existing '{0}'".format(spkg_name))
# Run the package's prerm script, if it exists.
# If an error occurs here we abort the uninstallation for now.
# This means a prerm script actually has the ability to abort an
# uninstallation, for example, if some manual intervention is needed
# to proceed.
try:
run_spkg_script(spkg_name, spkg_scripts, 'prerm', 'pre-uninstall')
except Exception as exc:
script_path = os.path.join(spkg_scripts, 'spkg-prerm')
print("Error: The pre-uninstall script for '{0}' failed; the "
"package will not be uninstalled, and some manual intervention "
"may be needed to repair the package's state before "
"uninstallation can proceed. Check further up in this log "
"for more details, or the pre-uninstall script itself at "
"{1}.".format(spkg_name, script_path), file=sys.stderr)
if isinstance(exc, subprocess.CalledProcessError):
sys.exit(exc.returncode)
else:
sys.exit(1)
def rmdir(dirname):
if os.path.isdir(dirname):
if not os.listdir(dirname):
if verbose:
print('rmdir "{}"'.format(dirname))
os.rmdir(dirname)
else:
print("Warning: Directory '{0}' not found".format(
dirname), file=sys.stderr)
# Remove the files; if a directory is empty after removing a file
# from it, remove the directory too.
for filename in files:
# Just in case: use lstrip to remove leading "/" from
# filename. See https://trac.sagemath.org/ticket/26013.
filename = pth.join(sage_local, filename.lstrip(os.sep))
dirname = pth.dirname(filename)
if os.path.lexists(filename):
if verbose:
print('rm "{}"'.format(filename))
os.remove(filename)
else:
print("Warning: File '{0}' not found".format(filename),
file=sys.stderr)
# Remove file's directory if it is now empty
rmdir(dirname)
# Run the package's postrm script, if it exists.
# If an error occurs here print a warning, but complete the
# uninstallation; otherwise we leave the package in a broken
# state--looking as though it's still 'installed', but with all its
# files removed.
try:
run_spkg_script(spkg_name, spkg_scripts, 'postrm',
'post-uninstall')
except Exception:
print("Warning: Error running the post-uninstall script for "
"'{0}'; the package will still be uninstalled, but "
"may have left behind some files or settings".format(
spkg_name), file=sys.stderr)
try:
shutil.rmtree(spkg_scripts)
except Exception:
pass
def remove_stamp_files(stamp_files, verbose=False):
# Finally, if all went well, delete all the stamp files.
for stamp_file in stamp_files:
print("Removing stamp file '{0}'".format(stamp_file))
os.remove(stamp_file)
def run_spkg_script(spkg_name, path, script_name, script_descr):
"""
Runs the specified ``spkg-<foo>`` script under the given ``path``,
if it exists.
"""
script = pth.join(path, 'spkg-{0}'.format(script_name))
if pth.exists(script):
print("Running {0} script for '{1}'".format(script_descr, spkg_name))
subprocess.check_call([script])
def dir_type(path):
"""
A custom argument 'type' for directory paths.
"""
if path and not pth.isdir(path):
raise argparse.ArgumentTypeError(
"'{0}' is not a directory".format(path))
return path
def spkg_type(pkg):
"""
A custom argument 'type' for spkgs--checks whether the given package name
is a known spkg.
"""
pkgbase = pth.join(PKGS, pkg)
if not pth.isdir(pkgbase):
raise argparse.ArgumentTypeError(
"'{0}' is not a known spkg".format(pkg))
return pkg
def make_parser():
"""Returns the command-line argument parser for sage-spkg-uninstall."""
doc_lines = __doc__.strip().splitlines()
parser = argparse.ArgumentParser(
description=doc_lines[0],
epilog='\n'.join(doc_lines[1:]).strip(),
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('spkg', type=spkg_type, help='the spkg to uninstall')
parser.add_argument('sage_local', type=dir_type, nargs='?',
default=os.environ.get('SAGE_LOCAL'),
help='the SAGE_LOCAL path (default: the $SAGE_LOCAL '
'environment variable if set)')
parser.add_argument('-v', '--verbose', action='store_true',
help='verbose output showing all files removed')
parser.add_argument('-k', '--keep-files', action='store_true',
help="only delete the package's installation record, "
"but do not remove files installed by the "
"package")
parser.add_argument('--debug', action='store_true', help=argparse.SUPPRESS)
return parser
def run(argv=None):
parser = make_parser()
args = parser.parse_args(argv if argv is not None else sys.argv[1:])
if args.sage_local is None:
print('Error: SAGE_LOCAL must be specified either at the command '
'line or in the $SAGE_LOCAL environment variable',
file=sys.stderr)
sys.exit(1)
try:
uninstall(args.spkg, args.sage_local, keep_files=args.keep_files,
verbose=args.verbose)
except Exception as exc:
print("Error during uninstallation of '{0}': {1}".format(
args.spkg, exc), file=sys.stderr)
if args.debug:
raise
sys.exit(1)
if __name__ == '__main__':
run()