2 # -*- coding: utf-8 -*-
4 # === This file is part of Calamares - <https://calamares.io> ===
6 # SPDX-FileCopyrightText: 2014 Teo Mrnjavac <teo@kde.org>
7 # SPDX-FileCopyrightText: 2014 Daniel Hillenbrand <codeworkx@bbqlinux.org>
8 # SPDX-FileCopyrightText: 2014 Philip Müller <philm@manjaro.org>
9 # SPDX-FileCopyrightText: 2017 Alf Gaida <agaida@siduction.org>
10 # SPDX-FileCopyrightText: 2019 Kevin Kofler <kevin.kofler@chello.at>
11 # SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
12 # SPDX-FileCopyrightText: 2020 Gabriel Craciunescu <crazy@frugalware.org>
13 # SPDX-License-Identifier: GPL-3.0-or-later
15 # Calamares is Free Software: see the License-Identifier above.
25 from libcalamares import *
26 from libcalamares.utils import mount
29 _ = gettext.translation("calamares-python",
30 localedir=utils.gettext_path(),
31 languages=utils.gettext_languages(),
32 fallback=True).gettext
35 return _("Filling up filesystems.")
37 # This is going to be changed from various methods
38 status = pretty_name()
40 def pretty_status_message():
45 Extraction routine using rsync.
51 __slots__ = ('source', 'sourcefs', 'destination', 'copied', 'total', 'exclude', 'excludeFile',
52 'mountPoint', 'weight')
54 def __init__(self, source, sourcefs, destination):
56 @p source is the source file name (might be an image file, or
58 @p sourcefs is a type indication; "file" is special, as is
60 @p destination is where the files from the source go. This is
61 **already** prefixed by rootMountPoint, so should be a
62 valid absolute path within the host system.
64 The members copied and total are filled in by the copying process.
67 self.sourcefs = sourcefs
68 self.destination = destination
70 self.excludeFile = None
73 self.mountPoint = None
77 return self.sourcefs == "file"
81 Counts the number of files this entry has.
85 if self.sourcefs == "squashfs":
86 fslist = subprocess.check_output(
87 ["unsquashfs", "-l", self.source]
90 elif self.sourcefs == "ext4":
91 fslist = subprocess.check_output(
92 ["find", self.mountPoint, "-type", "f"]
96 # Hasn't been mounted, copy directly; find handles both
97 # files and directories.
98 fslist = subprocess.check_output(["find", self.source, "-type", "f"])
100 self.total = len(fslist.splitlines())
103 def do_mount(self, base):
105 Mount given @p entry as loop device underneath @p base
107 A *file* entry (e.g. one with *sourcefs* set to *file*)
108 is not mounted and just ignored.
110 :param base: directory to place all the mounts in.
112 :returns: None, but throws if the mount failed
114 imgbasename = os.path.splitext(
115 os.path.basename(self.source))[0]
116 imgmountdir = os.path.join(base, imgbasename)
117 os.makedirs(imgmountdir, exist_ok=True)
119 # This is where it *would* go (files bail out before actually mounting)
120 self.mountPoint = imgmountdir
125 if os.path.isdir(self.source):
126 r = mount(self.source, imgmountdir, "", "--bind")
127 elif os.path.isfile(self.source):
128 r = mount(self.source, imgmountdir, self.sourcefs, "loop")
129 else: # self.source is a device
130 r = mount(self.source, imgmountdir, self.sourcefs, "")
133 utils.debug("Failed to mount '{}' (fs={}) (target={})".format(self.source, self.sourcefs, imgmountdir))
134 raise subprocess.CalledProcessError(r, "mount")
137 ON_POSIX = 'posix' in sys.builtin_module_names
140 def global_excludes():
142 List excludes for rsync.
145 extra_mounts = globalstorage.value("extraMounts")
146 if extra_mounts is None:
149 for extra_mount in extra_mounts:
150 mount_point = extra_mount["mountPoint"]
153 lst.extend(['--exclude', mount_point + '/'])
157 def file_copy(source, entry, progress_cb):
159 Extract given image using rsync.
161 :param source: Source file. This may be the place the entry's
162 image is mounted, or if it's a single file, the entry's source value.
163 :param entry: The UnpackEntry being copied.
164 :param progress_cb: A callback function for progress reporting.
165 Takes a number and a total-number.
169 dest = entry.destination
171 # Environment used for executing rsync properly
172 # Setting locale to C (fix issue with tr_TR locale)
174 at_env["LC_ALL"] = "C"
176 # `source` *must* end with '/' otherwise a directory named after the source
177 # will be created in `dest`: ie if `source` is "/foo/bar" and `dest` is
178 # "/dest", then files will be copied in "/dest/bar".
179 if not source.endswith("/") and not os.path.isfile(source):
182 num_files_total_local = 0
183 num_files_copied = 0 # Gets updated through rsync output
185 args = ['rsync', '-aHAXr', '--filter=-x trusted.overlay.*']
186 args.extend(global_excludes())
187 if entry.excludeFile:
188 args.extend(["--exclude-from=" + entry.excludeFile])
190 for f in entry.exclude:
191 args.extend(["--exclude", f])
192 args.extend(['--progress', source, dest])
193 process = subprocess.Popen(
195 stdout=subprocess.PIPE, close_fds=ON_POSIX
197 # last_num_files_copied trails num_files_copied, and whenever at least 107 more
198 # files (file_count_chunk) have been copied, progress is reported and
199 # last_num_files_copied is updated. The chunk size isn't "tidy"
200 # so that all the digits of the progress-reported number change.
202 last_num_files_copied = 0
203 last_timestamp_reported = time.time()
204 file_count_chunk = 107
206 for line in iter(process.stdout.readline, b''):
207 # rsync outputs progress in parentheses. Each line will have an
208 # xfer and a chk item (either ir-chk or to-chk) as follows:
210 # - xfer#x => Interpret it as 'file copy try no. x'
211 # - ir-chk=x/y, where:
212 # - x = number of files yet to be checked
213 # - y = currently calculated total number of files.
214 # - to-chk=x/y, which is similar and happens once the ir-chk
215 # phase (collecting total files) is over.
217 # If you're copying directory with some links in it, the xfer#
218 # might not be a reliable counter (for one increase of xfer, many
219 # files may be created).
220 m = re.findall(r'xfr#(\d+), ..-chk=(\d+)/(\d+)', line.decode())
223 # we've got a percentage update
224 num_files_remaining = int(m[0][1])
225 num_files_total_local = int(m[0][2])
226 # adjusting the offset so that progressbar can be continuesly drawn
227 num_files_copied = num_files_total_local - num_files_remaining
230 if (num_files_copied - last_num_files_copied >= file_count_chunk) or (now - last_timestamp_reported > 0.5):
231 last_num_files_copied = num_files_copied
232 last_timestamp_reported = now
233 progress_cb(num_files_copied, num_files_total_local)
236 progress_cb(num_files_copied, num_files_total_local) # Push towards 100%
238 # Mark this entry as really done
239 entry.copied = entry.total
241 # 23 is the return code rsync returns if it cannot write extended
242 # attributes (with -X) because the target file system does not support it,
243 # e.g., the FAT EFI system partition. We need -X because distributions
244 # using file system capabilities and/or SELinux require the extended
245 # attributes. But distributions using SELinux may also have SELinux labels
246 # set on files under /boot/efi, and rsync complains about those. The only
247 # clean way would be to split the rsync into one with -X and
248 # --exclude /boot/efi and a separate one without -X for /boot/efi, but only
249 # if /boot/efi is actually an EFI system partition. For now, this hack will
250 # have to do. See also:
251 # https://bugzilla.redhat.com/show_bug.cgi?id=868755#c50
252 # for the same issue in Anaconda, which uses a similar workaround.
253 if process.returncode != 0 and process.returncode != 23:
254 utils.warning("rsync failed with error code {}.".format(process.returncode))
255 return _("rsync failed with error code {}.").format(process.returncode)
260 class UnpackOperation:
262 Extraction routine using unsquashfs.
267 def __init__(self, entries):
268 self.entries = entries
269 self.entry_for_source = dict((x.source, x) for x in self.entries)
270 self.total_weight = sum([e.weight for e in entries])
272 def report_progress(self):
274 Pass progress to user interface
279 current_done = 0 # Files count in the current entry
281 complete_weight = 0 # This much weight already finished
282 for entry in self.entries:
284 # Total 0 hasn't counted yet
286 if entry.total == entry.copied:
287 complete_weight += entry.weight
290 # There is at most *one* entry in-progress
291 current_total = entry.total
292 current_done = entry.copied
293 complete_weight += entry.weight * ( 1.0 * current_done ) / current_total
296 if current_total > 0:
297 progress = ( 1.0 * complete_weight ) / self.total_weight
300 status = _("Unpacking image {}/{}, file {}/{}").format((complete_count+1), len(self.entries), current_done, current_total)
301 job.setprogress(progress)
305 Extract given image using unsquashfs.
310 source_mount_path = tempfile.mkdtemp()
314 for entry in self.entries:
315 status = _("Starting to unpack {}").format(entry.source)
316 job.setprogress( ( 1.0 * complete ) / len(self.entries) )
317 entry.do_mount(source_mount_path)
318 entry.do_count() # Fill in the entry.total
320 self.report_progress()
321 error_msg = self.unpack_image(entry, entry.mountPoint)
324 return (_("Failed to unpack image \"{}\"").format(entry.source),
330 shutil.rmtree(source_mount_path, ignore_errors=True, onerror=None)
333 def unpack_image(self, entry, imgmountdir):
341 def progress_cb(copied, total):
342 """ Copies file to given destination target.
346 entry.copied = copied
347 if total > entry.total:
349 self.report_progress()
353 source = entry.source
357 return file_copy(source, entry, progress_cb)
359 if not entry.is_file():
360 subprocess.check_call(["umount", "-l", imgmountdir])
363 def get_supported_filesystems_kernel():
365 Reads /proc/filesystems (the list of supported filesystems
366 for the current kernel) and returns a list of (names of)
369 PATH_PROCFS = '/proc/filesystems'
371 if os.path.isfile(PATH_PROCFS) and os.access(PATH_PROCFS, os.R_OK):
372 with open(PATH_PROCFS, 'r') as procfile:
373 filesystems = procfile.read()
374 filesystems = filesystems.replace(
375 "nodev", "").replace("\t", "").splitlines()
381 def get_supported_filesystems():
383 Returns a list of all the supported filesystems
384 (valid values for the *sourcefs* key in an item.
386 return ["file"] + get_supported_filesystems_kernel()
389 def repair_root_permissions(root_mount_point):
391 If the / of the system gets permission 777, change it down
392 to 755. Any other permission is left alone. This
393 works around standard behavior from squashfs where
394 permissions are (easily, accidentally) set to 777.
396 existing_root_mode = os.stat(root_mount_point).st_mode & 0o777
397 if existing_root_mode == 0o777:
399 os.chmod(root_mount_point, 0o755) # Want / to be rwxr-xr-x
401 utils.warning("Could not set / to safe permissions: {}".format(e))
405 def extract_weight(entry):
407 Given @p entry, a dict representing a single entry in
408 the *unpack* list, returns its weight (1, or whatever is
409 set if it is sensible).
411 w = entry.get("weight", None)
415 return wi if wi > 0 else 1
417 utils.warning("*weight* setting {!r} is not valid.".format(w))
419 utils.warning("*weight* setting {!r} must be number.".format(w))
427 root_mount_point = globalstorage.value("rootMountPoint")
429 if not root_mount_point:
430 utils.warning("No mount point for root partition")
431 return (_("No mount point for root partition"),
432 _("globalstorage does not contain a \"rootMountPoint\" key, "
435 if not os.path.exists(root_mount_point):
436 utils.warning("Bad root mount point \"{}\"".format(root_mount_point))
437 return (_("Bad mount point for root partition"),
438 _("rootMountPoint is \"{}\", which does not "
439 "exist, doing nothing").format(root_mount_point))
441 supported_filesystems = get_supported_filesystems()
443 # Bail out before we start when there are obvious problems
444 # - unsupported filesystems
445 # - non-existent sources
446 # - missing tools for specific FS
447 for entry in job.configuration["unpack"]:
448 source = os.path.abspath(entry["source"])
449 sourcefs = entry["sourcefs"]
451 if sourcefs not in supported_filesystems:
452 utils.warning("The filesystem for \"{}\" ({}) is not supported by your current kernel".format(source, sourcefs))
453 utils.warning(" ... modprobe {} may solve the problem".format(sourcefs))
454 return (_("Bad unsquash configuration"),
455 _("The filesystem for \"{}\" ({}) is not supported by your current kernel").format(source, sourcefs))
456 if not os.path.exists(source):
457 utils.warning("The source filesystem \"{}\" does not exist".format(source))
458 return (_("Bad unsquash configuration"),
459 _("The source filesystem \"{}\" does not exist").format(source))
460 if sourcefs == "squashfs":
461 if shutil.which("unsquashfs") is None:
462 utils.warning("Failed to find unsquashfs")
464 return (_("Failed to unpack image \"{}\"").format(self.source),
465 _("Failed to find unsquashfs, make sure you have the squashfs-tools package installed"))
470 for entry in job.configuration["unpack"]:
471 source = os.path.abspath(entry["source"])
472 sourcefs = entry["sourcefs"]
473 destination = os.path.abspath(root_mount_point + entry["destination"])
475 if not os.path.isdir(destination) and sourcefs != "file":
476 utils.warning(("The destination \"{}\" in the target system is not a directory").format(destination))
478 return (_("Bad unsquash configuration"),
479 _("The destination \"{}\" in the target system is not a directory").format(destination))
481 utils.debug(".. assuming that the previous targets will create that directory.")
483 unpack.append(UnpackEntry(source, sourcefs, destination))
485 if entry.get("exclude", None):
486 unpack[-1].exclude = entry["exclude"]
487 if entry.get("excludeFile", None):
488 unpack[-1].excludeFile = entry["excludeFile"]
489 unpack[-1].weight = extract_weight(entry)
493 repair_root_permissions(root_mount_point)
495 unpackop = UnpackOperation(unpack)
496 return unpackop.run()
498 repair_root_permissions(root_mount_point)