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.
28 _ = gettext.translation("calamares-python",
29 localedir=libcalamares.utils.gettext_path(),
30 languages=libcalamares.utils.gettext_languages(),
31 fallback=True).gettext
34 return _("Filling up filesystems.")
36 # This is going to be changed from various methods
37 status = pretty_name()
39 def pretty_status_message():
44 Extraction routine using rsync.
50 __slots__ = ('source', 'sourcefs', 'destination', 'copied', 'total', 'exclude', 'excludeFile',
51 'mountPoint', 'weight')
53 def __init__(self, source, sourcefs, destination):
55 @p source is the source file name (might be an image file, or
57 @p sourcefs is a type indication; "file" is special, as is
59 @p destination is where the files from the source go. This is
60 **already** prefixed by rootMountPoint, so should be a
61 valid absolute path within the host system.
63 The members copied and total are filled in by the copying process.
66 self.sourcefs = sourcefs
67 self.destination = destination
69 self.excludeFile = None
72 self.mountPoint = None
76 return self.sourcefs == "file"
80 Counts the number of files this entry has.
84 if self.sourcefs == "squashfs":
85 fslist = subprocess.check_output(
86 ["unsquashfs", "-l", self.source]
89 elif self.sourcefs == "ext4":
90 fslist = subprocess.check_output(
91 ["find", self.mountPoint, "-type", "f"]
95 # Hasn't been mounted, copy directly; find handles both
96 # files and directories.
97 fslist = subprocess.check_output(["find", self.source, "-type", "f"])
99 self.total = len(fslist.splitlines())
102 def do_mount(self, base):
104 Mount given @p entry as loop device underneath @p base
106 A *file* entry (e.g. one with *sourcefs* set to *file*)
107 is not mounted and just ignored.
109 :param base: directory to place all the mounts in.
111 :returns: None, but throws if the mount failed
113 imgbasename = os.path.splitext(
114 os.path.basename(self.source))[0]
115 imgmountdir = os.path.join(base, imgbasename)
116 os.makedirs(imgmountdir, exist_ok=True)
118 # This is where it *would* go (files bail out before actually mounting)
119 self.mountPoint = imgmountdir
124 if os.path.isdir(self.source):
125 r = libcalamares.utils.mount(self.source, imgmountdir, "", "--bind")
126 elif os.path.isfile(self.source):
127 r = libcalamares.utils.mount(self.source, imgmountdir, self.sourcefs, "loop")
128 else: # self.source is a device
129 r = libcalamares.utils.mount(self.source, imgmountdir, self.sourcefs, "")
132 libcalamares.utils.debug("Failed to mount '{}' (fs={}) (target={})".format(self.source, self.sourcefs, imgmountdir))
133 raise subprocess.CalledProcessError(r, "mount")
136 ON_POSIX = 'posix' in sys.builtin_module_names
139 def global_excludes():
141 List excludes for rsync.
144 extra_mounts = libcalamares.globalstorage.value("extraMounts")
145 if extra_mounts is None:
148 for extra_mount in extra_mounts:
149 mount_point = extra_mount["mountPoint"]
152 lst.extend(['--exclude', mount_point + '/'])
156 def file_copy(source, entry, progress_cb):
158 Extract given image using rsync.
160 :param source: Source file. This may be the place the entry's
161 image is mounted, or if it's a single file, the entry's source value.
162 :param entry: The UnpackEntry being copied.
163 :param progress_cb: A callback function for progress reporting.
164 Takes a number and a total-number.
168 dest = entry.destination
170 # Environment used for executing rsync properly
171 # Setting locale to C (fix issue with tr_TR locale)
173 at_env["LC_ALL"] = "C"
175 # `source` *must* end with '/' otherwise a directory named after the source
176 # will be created in `dest`: ie if `source` is "/foo/bar" and `dest` is
177 # "/dest", then files will be copied in "/dest/bar".
178 if not source.endswith("/") and not os.path.isfile(source):
181 num_files_total_local = 0
182 num_files_copied = 0 # Gets updated through rsync output
184 args = ['rsync', '-aHAXr', '--filter=-x trusted.overlay.*']
185 args.extend(global_excludes())
186 if entry.excludeFile:
187 args.extend(["--exclude-from=" + entry.excludeFile])
189 for f in entry.exclude:
190 args.extend(["--exclude", f])
191 args.extend(['--progress', source, dest])
192 process = subprocess.Popen(
194 stdout=subprocess.PIPE, close_fds=ON_POSIX
196 # last_num_files_copied trails num_files_copied, and whenever at least 107 more
197 # files (file_count_chunk) have been copied, progress is reported and
198 # last_num_files_copied is updated. The chunk size isn't "tidy"
199 # so that all the digits of the progress-reported number change.
201 last_num_files_copied = 0
202 last_timestamp_reported = time.time()
203 file_count_chunk = 107
205 for line in iter(process.stdout.readline, b''):
206 # rsync outputs progress in parentheses. Each line will have an
207 # xfer and a chk item (either ir-chk or to-chk) as follows:
209 # - xfer#x => Interpret it as 'file copy try no. x'
210 # - ir-chk=x/y, where:
211 # - x = number of files yet to be checked
212 # - y = currently calculated total number of files.
213 # - to-chk=x/y, which is similar and happens once the ir-chk
214 # phase (collecting total files) is over.
216 # If you're copying directory with some links in it, the xfer#
217 # might not be a reliable counter (for one increase of xfer, many
218 # files may be created).
219 m = re.findall(r'xfr#(\d+), ..-chk=(\d+)/(\d+)', line.decode())
222 # we've got a percentage update
223 num_files_remaining = int(m[0][1])
224 num_files_total_local = int(m[0][2])
225 # adjusting the offset so that progressbar can be continuesly drawn
226 num_files_copied = num_files_total_local - num_files_remaining
229 if (num_files_copied - last_num_files_copied >= file_count_chunk) or (now - last_timestamp_reported > 0.5):
230 last_num_files_copied = num_files_copied
231 last_timestamp_reported = now
232 progress_cb(num_files_copied, num_files_total_local)
235 progress_cb(num_files_copied, num_files_total_local) # Push towards 100%
237 # Mark this entry as really done
238 entry.copied = entry.total
240 # 23 is the return code rsync returns if it cannot write extended
241 # attributes (with -X) because the target file system does not support it,
242 # e.g., the FAT EFI system partition. We need -X because distributions
243 # using file system capabilities and/or SELinux require the extended
244 # attributes. But distributions using SELinux may also have SELinux labels
245 # set on files under /boot/efi, and rsync complains about those. The only
246 # clean way would be to split the rsync into one with -X and
247 # --exclude /boot/efi and a separate one without -X for /boot/efi, but only
248 # if /boot/efi is actually an EFI system partition. For now, this hack will
249 # have to do. See also:
250 # https://bugzilla.redhat.com/show_bug.cgi?id=868755#c50
251 # for the same issue in Anaconda, which uses a similar workaround.
252 if process.returncode != 0 and process.returncode != 23:
253 libcalamares.utils.warning("rsync failed with error code {}.".format(process.returncode))
254 return _("rsync failed with error code {}.").format(process.returncode)
259 class UnpackOperation:
261 Extraction routine using unsquashfs.
266 def __init__(self, entries):
267 self.entries = entries
268 self.entry_for_source = dict((x.source, x) for x in self.entries)
269 self.total_weight = sum([e.weight for e in entries])
271 def report_progress(self):
273 Pass progress to user interface
278 current_done = 0 # Files count in the current entry
280 complete_weight = 0 # This much weight already finished
281 for entry in self.entries:
283 # Total 0 hasn't counted yet
285 if entry.total == entry.copied:
286 complete_weight += entry.weight
289 # There is at most *one* entry in-progress
290 current_total = entry.total
291 current_done = entry.copied
292 complete_weight += entry.weight * ( 1.0 * current_done ) / current_total
295 if current_total > 0:
296 progress = ( 1.0 * complete_weight ) / self.total_weight
299 status = _("Unpacking image {}/{}, file {}/{}").format((complete_count+1), len(self.entries), current_done, current_total)
300 libcalamares.job.setprogress(progress)
304 Extract given image using unsquashfs.
309 source_mount_path = tempfile.mkdtemp()
313 for entry in self.entries:
314 status = _("Starting to unpack {}").format(entry.source)
315 libcalamares.job.setprogress( ( 1.0 * complete ) / len(self.entries) )
316 entry.do_mount(source_mount_path)
317 entry.do_count() # Fill in the entry.total
319 self.report_progress()
320 error_msg = self.unpack_image(entry, entry.mountPoint)
323 return (_("Failed to unpack image \"{}\"").format(entry.source),
329 shutil.rmtree(source_mount_path, ignore_errors=True, onerror=None)
332 def unpack_image(self, entry, imgmountdir):
340 def progress_cb(copied, total):
341 """ Copies file to given destination target.
345 entry.copied = copied
346 if total > entry.total:
348 self.report_progress()
352 source = entry.source
356 return file_copy(source, entry, progress_cb)
358 if not entry.is_file():
359 subprocess.check_call(["umount", "-l", imgmountdir])
362 def get_supported_filesystems_kernel():
364 Reads /proc/filesystems (the list of supported filesystems
365 for the current kernel) and returns a list of (names of)
368 PATH_PROCFS = '/proc/filesystems'
370 if os.path.isfile(PATH_PROCFS) and os.access(PATH_PROCFS, os.R_OK):
371 with open(PATH_PROCFS, 'r') as procfile:
372 filesystems = procfile.read()
373 filesystems = filesystems.replace(
374 "nodev", "").replace("\t", "").splitlines()
380 def get_supported_filesystems():
382 Returns a list of all the supported filesystems
383 (valid values for the *sourcefs* key in an item.
385 return ["file"] + get_supported_filesystems_kernel()
388 def repair_root_permissions(root_mount_point):
390 If the / of the system gets permission 777, change it down
391 to 755. Any other permission is left alone. This
392 works around standard behavior from squashfs where
393 permissions are (easily, accidentally) set to 777.
395 existing_root_mode = os.stat(root_mount_point).st_mode & 0o777
396 if existing_root_mode == 0o777:
398 os.chmod(root_mount_point, 0o755) # Want / to be rwxr-xr-x
400 libcalamares.utils.warning("Could not set / to safe permissions: {}".format(e))
404 def extract_weight(entry):
406 Given @p entry, a dict representing a single entry in
407 the *unpack* list, returns its weight (1, or whatever is
408 set if it is sensible).
410 w = entry.get("weight", None)
414 return wi if wi > 0 else 1
416 libcalamares.utils.warning("*weight* setting {!r} is not valid.".format(w))
418 libcalamares.utils.warning("*weight* setting {!r} must be number.".format(w))
426 root_mount_point = libcalamares.globalstorage.value("rootMountPoint")
428 if not root_mount_point:
429 libcalamares.utils.warning("No mount point for root partition")
430 return (_("No mount point for root partition"),
431 _("globalstorage does not contain a \"rootMountPoint\" key, "
434 if not os.path.exists(root_mount_point):
435 libcalamares.utils.warning("Bad root mount point \"{}\"".format(root_mount_point))
436 return (_("Bad mount point for root partition"),
437 _("rootMountPoint is \"{}\", which does not "
438 "exist, doing nothing").format(root_mount_point))
440 supported_filesystems = get_supported_filesystems()
442 # Bail out before we start when there are obvious problems
443 # - unsupported filesystems
444 # - non-existent sources
445 # - missing tools for specific FS
446 for entry in libcalamares.job.configuration["unpack"]:
447 source = os.path.abspath(entry["source"])
448 sourcefs = entry["sourcefs"]
450 if sourcefs not in supported_filesystems:
451 libcalamares.utils.warning("The filesystem for \"{}\" ({}) is not supported by your current kernel".format(source, sourcefs))
452 libcalamares.utils.warning(" ... modprobe {} may solve the problem".format(sourcefs))
453 return (_("Bad unsquash configuration"),
454 _("The filesystem for \"{}\" ({}) is not supported by your current kernel").format(source, sourcefs))
455 if not os.path.exists(source):
456 libcalamares.utils.warning("The source filesystem \"{}\" does not exist".format(source))
457 return (_("Bad unsquash configuration"),
458 _("The source filesystem \"{}\" does not exist").format(source))
459 if sourcefs == "squashfs":
460 if shutil.which("unsquashfs") is None:
461 libcalamares.utils.warning("Failed to find unsquashfs")
463 return (_("Failed to unpack image \"{}\"").format(self.source),
464 _("Failed to find unsquashfs, make sure you have the squashfs-tools package installed"))
469 for entry in libcalamares.job.configuration["unpack"]:
470 source = os.path.abspath(entry["source"])
471 sourcefs = entry["sourcefs"]
472 destination = os.path.abspath(root_mount_point + entry["destination"])
474 if not os.path.isdir(destination) and sourcefs != "file":
475 libcalamares.utils.warning(("The destination \"{}\" in the target system is not a directory").format(destination))
477 return (_("Bad unsquash configuration"),
478 _("The destination \"{}\" in the target system is not a directory").format(destination))
480 libcalamares.utils.debug(".. assuming that the previous targets will create that directory.")
482 unpack.append(UnpackEntry(source, sourcefs, destination))
484 if entry.get("exclude", None):
485 unpack[-1].exclude = entry["exclude"]
486 if entry.get("excludeFile", None):
487 unpack[-1].excludeFile = entry["excludeFile"]
488 unpack[-1].weight = extract_weight(entry)
492 repair_root_permissions(root_mount_point)
494 unpackop = UnpackOperation(unpack)
495 return unpackop.run()
497 repair_root_permissions(root_mount_point)