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