-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathremove_common.py
executable file
·302 lines (250 loc) · 11.3 KB
/
remove_common.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
#!/usr/bin/env python3
""" Removes files that are already in base snaps or have been generated
in a previous part. Useful to remove files added by stage-packages
due to dependencies, but that aren't required because they are
already available in core22, gnome-42-2204 or gtk-common-themes,
or have already been built by a previous part. """
import sys
import os
import glob
import argparse
import fnmatch
try:
import yaml
except:
print("YAML module not found. Please, add 'python3-yaml' to the 'build-packages' list.")
pass
parser = argparse.ArgumentParser(prog="remove_common", description="An utility to remove from snaps files that are already available in extensions")
parser.add_argument('extension', nargs='*', default=[])
parser.add_argument('-e', '--exclude', nargs='+', help="A list of files and directories to exclude from checking")
parser.add_argument('-m', '--map', nargs='+', default=[], help="A list of snap_name:path pairs")
parser.add_argument('-v', '--verbose', action='store_true', default=False, help="Show extra info")
parser.add_argument('-q', '--quiet', action='store_true', default=False, help="Don't show any message")
args = parser.parse_args()
# specific case for themed icons
global_excludes = ['usr/share/icons/*/index.theme']
global_maps = ['gtk-common-themes:usr']
def get_snapcraft_yaml():
"""Returns a string with the full path of the snapcraft file.
Returns
-------
string
The full path of the snapcraft.yaml file, or None if no file is found.
"""
if 'CRAFT_PROJECT_DIR' not in os.environ:
return None
project_folder = os.environ['CRAFT_PROJECT_DIR']
snapcraft_file_path = os.path.join(project_folder, "snapcraft.yaml")
if not os.path.exists(snapcraft_file_path):
snapcraft_file_path = os.path.join(project_folder, "snap", "snapcraft.yaml")
if not os.path.exists(snapcraft_file_path):
return None
return snapcraft_file_path
def get_extension_list(cmdline_extensions):
"""Returns an array with the extensions for this project.
Parameters
----------
cmdline_extensions : array of strings
an array with the list of extensions passed by the command line.
If no extensions were passed, it must be a zero-length array.
Returns
-------
array of strings
an array with the list of extensions desired. If no extensions are
defined, it will return a zero-length array.
Raises
------
FileNotFoundError
If the snapcraft.yaml file can't be found.
"""
if len(cmdline_extensions) != 0:
return cmdline_extensions
snapcraft_file = get_snapcraft_yaml()
if snapcraft_file is None:
raise FileNotFoundError("There is no snapcraft.yaml file in the project folder")
with open(snapcraft_file, "r") as snapcraft_stream:
snapcraft_data = yaml.load(snapcraft_stream, Loader=yaml.Loader)
parts_data = snapcraft_data["parts"]
extensions = []
for part_name in parts_data:
part_data = parts_data[part_name]
if "build-snaps" not in part_data:
continue
for extension in part_data["build-snaps"]:
if extension not in extensions:
extensions.append(extension)
return extensions
def generate_mappings(predefined_mappings, cmdline_mappings):
"""Parses the specific mappings for each snap
It receives both the predefined mappings and the mappings passed by
command line, and returns a dictionary with each snap and its corresponding
mapping path.
Parameters
----------
predefined_mappings : array of strings
An array of strings, each one in the format snap_name:mapping. Used
for the "known mappings", like the one for gtk-common-themes.
cmdline_mappings : array of strings
An array of strings as received from argparse with the snap_name:mapping
pairs.
Returns
-------
dictionary
A dictionary where each key is a string with a snap name and the
value is another string with the mapping path for that snap, ended
in '/'.
Raises
------
SyntaxError
If the format of any of the mapping strings is not valid
"""
mappings = {}
for map in predefined_mappings + cmdline_mappings:
elements = map.split(":")
if len(elements) != 2:
raise SyntaxError("Error in mapping. It must be in the format snap_name:path", {'filename': 'remove_common.py', 'text': map, 'lineno': 0, 'offset': 0})
if elements[1] == '/':
raise SyntaxError("The mapping can't be '/'", {'filename': 'remove_common.py', 'text': map, 'lineno': 0, 'offset': 0})
while elements[1][0] == '/':
elements[1] = elements[1][1:]
if elements[1][-1] != '/':
elements[1] += '/'
mappings[elements[0]] = elements[1]
return mappings
def generate_extensions_paths(extensions, mappings):
"""Generates the list of extensions paths from the list of extensions
This list has the paths of each extension used, to know where to search
for duplicates. Also, it contains the corresponding map for each path.
Parameters
----------
extensions : array of strings
An array of strings with the names of the extensions used in this snap.
mappings : dictionary
A dictionary where each key is a string with a snap name and the
value is another string with the mapping path for that snap, ended
in '/'.
Returns
-------
array of tuples
An array of tuples, where the first element of each tuple is a string
to the contents of each extension, and the second element is the mapping
for that path, or None if no mapping is needed.
"""
folders = []
for snap in extensions:
path = f"/snap/{snap}/current"
map_path = mappings[snap] if snap in mappings else None
folders.append((path, map_path))
return folders
def check_if_exists(extensions_paths, relative_file_path, verbose):
"""Checks if an specific file does exist in any of the base paths.
Checks if the specified file at `relative_file_path` does exist in
any of the paths included in the `extensions_paths` array, taking into
account the mapping specified.
Mapping is paramount for some extension snaps like gtk-common-themes.
In this specific case, the snap contains the `share` folder directly
at its root folder, while any other snap would have it inside the
`usr` folder. Thus, for that extension snap, the map must be `usr`
to instruct this function to remove `usr` from the beginning of the
relative path when checking for a file inside gtk-common-themes.
Parameters
----------
extensions_paths : array of tuples with two elements
An array with tuples containing each one a string with the root
path for one of the extensions snap, and, if required, another
string with the mapping for that snap (or None if no mapping is
required for that snap).
relative_file_path : string
The file path to search in the folder list, relative to the
snap root.
Returns
-------
boolean
It returns True if the file does exist in, at least, one of the
specified snaps, and false if it doesn't exist in any of them.
"""
# Checks if an specific file does exist in any of the base paths
for folder, map_path in extensions_paths:
if (map_path is not None) and relative_file_path.startswith(map_path):
relative_file_path2 = relative_file_path[len(map_path):]
if relative_file_path2[0] == '/':
relative_file_path2 = relative_file_path2[1:]
else:
relative_file_path2 = relative_file_path
check_path = os.path.join(folder, relative_file_path2)
if os.path.exists(check_path):
if verbose:
print(f"The path {relative_file_path} has been found inside {folder} with map {map_path}: {relative_file_path2}")
return True
return False
def main(snap_folder, extensions_paths, exclude_list=[], verbose=False, quiet=True):
"""Main function
Searches each file in 'snap_folder' inside each path in 'extensions_paths'
to check if it is already available there, deleting it in that case, unless
it matches any of the rules in 'exclude_list'.
The check is done based only on the relative path and file name relative to
'snap_folder', searching that inside each path in 'extensions_paths', although
taking into account the mapping.
Parameters
----------
snap_folder : string
The path of the folder where the staged .deb have been uncompressed (usually
CRAFT_PART_INSTALL)
extensions_paths : array of tuples with two elements
An array with tuples containing each one a string with the root
path for one of the extensions snap, and, if required, another
string with the mapping for that snap (or None if no mapping is
required for that snap).
exclude_list : array of strings
A list of fnmatch rules for excluding files and/or paths
verbose : bool, optional
Show extra verbose information, by default False
quiet : bool, optional
Don't show messages, by default False
"""
duplicated_bytes = 0
for full_file_path in glob.glob(os.path.join(snap_folder, "**/*"), recursive=True):
if not os.path.isfile(full_file_path) and not os.path.islink(full_file_path):
continue
relative_file_path = full_file_path[len(snap_folder):]
if relative_file_path[0] == '/':
relative_file_path = relative_file_path[1:]
do_exclude = False
for exclude in exclude_list:
if fnmatch.fnmatch(relative_file_path, exclude):
if verbose:
print(f"Excluding {relative_file_path} with rule {exclude}")
do_exclude = True
break
if do_exclude:
continue
if check_if_exists(extensions_paths, relative_file_path, verbose):
if os.path.isfile(full_file_path):
duplicated_bytes += os.stat(full_file_path).st_size
os.remove(full_file_path)
if verbose:
print(f"Removing duplicated file {relative_file_path} {full_file_path}")
if not quiet:
print(f"Removed {duplicated_bytes} bytes in duplicated files")
if __name__ == "__main__":
verbose = args.verbose
exclude = args.exclude
extensions = args.extension
quiet = args.quiet
mapping = {}
if exclude is not None:
global_excludes += exclude
extensions = get_extension_list(args.extension)
if len(extensions) == 0:
print("Called remove_common.py without a list of snaps, and no 'build-snaps' entry in the snapcraft.yaml file. Aborting.")
sys.exit(1)
mappings = generate_mappings(global_maps, args.map)
if verbose:
print(f"Removing duplicates already in {extensions}")
extensions_paths = generate_extensions_paths(extensions, mappings)
extensions_paths.append((os.environ["CRAFT_STAGE"], None))
# This is the folder where to check for duplicates that are already
# in other snaps, or in the stage because they were built in other
# parts.
snap_folder = os.environ["CRAFT_PART_INSTALL"]
main(snap_folder, extensions_paths, global_excludes, verbose, quiet)