]>
git.ipfire.org Git - people/stevee/pakfire.git/blob - python/pakfire/builder.py
2 ###############################################################################
4 # Pakfire - The IPFire package management system #
5 # Copyright (C) 2011 Pakfire development team #
7 # This program is free software: you can redistribute it and/or modify #
8 # it under the terms of the GNU General Public License as published by #
9 # the Free Software Foundation, either version 3 of the License, or #
10 # (at your option) any later version. #
12 # This program is distributed in the hope that it will be useful, #
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
15 # GNU General Public License for more details. #
17 # You should have received a copy of the GNU General Public License #
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. #
20 ###############################################################################
37 import packages
.packager
43 log
= logging
.getLogger("pakfire")
45 from system
import system
46 from constants
import *
48 from errors
import BuildError
, BuildRootLocked
, Error
51 BUILD_LOG_HEADER
= """
53 | _ \ __ _| | __/ _(_)_ __ ___ | |__ _ _(_) | __| | ___ _ __
54 | |_) / _` | |/ / |_| | '__/ _ \ | '_ \| | | | | |/ _` |/ _ \ '__|
55 | __/ (_| | <| _| | | | __/ | |_) | |_| | | | (_| | __/ |
56 |_| \__,_|_|\_\_| |_|_| \___| |_.__/ \__,_|_|_|\__,_|\___|_|
59 Host : %(hostname)s (%(host_arch)s)
64 class BuildEnviron(object):
65 # The version of the kernel this machine is running.
66 kernel_version
= os
.uname()[2]
68 def __init__(self
, filename
=None, distro_config
=None, build_id
=None, logfile
=None,
69 builder_mode
="release", use_cache
=None, **pakfire_args
):
71 assert builder_mode
in ("development", "release",)
72 self
.mode
= builder_mode
74 # Disable the build repository in release mode.
75 if self
.mode
== "release":
76 if pakfire_args
.has_key("disable_repos") and pakfire_args
["disable_repos"]:
77 pakfire_args
["disable_repos"] += ["build",]
79 pakfire_args
["disable_repos"] = ["build",]
81 # Save the build id and generate one if no build id was provided.
83 build_id
= "%s" % uuid
.uuid4()
85 self
.build_id
= build_id
89 self
.log
= log
.getChild(self
.build_id
)
90 # Propage everything to the root logger that we will see something
92 self
.log
.propagate
= 1
93 self
.log
.setLevel(logging
.INFO
)
95 # Add the given logfile to the logger.
96 h
= logging
.FileHandler(logfile
)
97 self
.log
.addHandler(h
)
99 # Format the log output for the file.
100 f
= logger
.BuildFormatter()
103 # If no logile was given, we use the root logger.
104 self
.log
= logging
.getLogger("pakfire")
106 # Log information about pakfire and some more information, when we
107 # are running in release mode.
108 if self
.mode
== "release":
110 "host_arch" : system
.arch
,
111 "hostname" : system
.hostname
,
112 "time" : time
.strftime("%a, %d %b %Y %H:%M:%S +0000", time
.gmtime()),
113 "version" : "Pakfire %s" % PAKFIRE_VERSION
,
116 for line
in BUILD_LOG_HEADER
.splitlines():
117 self
.log
.info(line
% logdata
)
119 # Create pakfire instance.
120 if pakfire_args
.has_key("mode"):
121 del pakfire_args
["mode"]
122 self
.pakfire
= base
.Pakfire(mode
="builder", distro_config
=distro_config
, **pakfire_args
)
123 self
.distro
= self
.pakfire
.distro
124 self
.path
= self
.pakfire
.path
126 # Check if this host can build the requested architecture.
127 if not self
.arch
in self
.pakfire
.config
.supported_arches
:
128 raise BuildError
, _("Cannot build for %s on this host.") % self
.arch
130 # Where do we put the result?
131 self
.resultdir
= os
.path
.join(self
.path
, "result")
133 # Check weather to use or not use the cache.
134 if use_cache
is None:
135 # If use_cache is None, the user did not provide anything and
137 if self
.mode
== "development":
142 self
.use_cache
= use_cache
145 # If we have a plain makefile, we first build a source package and go with that.
147 if filename
.endswith(".%s" % MAKEFILE_EXTENSION
):
148 pkg
= packages
.Makefile(self
.pakfire
, filename
)
149 pkg
.dist([self
.resultdir
,])
151 filename
= os
.path
.join(self
.resultdir
, "src", pkg
.package_filename
)
152 assert os
.path
.exists(filename
), filename
154 # Open source package.
155 self
.pkg
= packages
.SourcePackage(self
.pakfire
, None, filename
)
156 assert self
.pkg
, filename
158 # Log the package information.
159 self
.log
.info(_("Package information:"))
160 for line
in self
.pkg
.dump(long=True).splitlines():
161 self
.log
.info(" %s" % line
)
164 # Path where we extract the package and put all the source files.
165 self
.build_dir
= os
.path
.join(self
.path
, "usr/src/packages", self
.pkg
.friendly_name
)
170 # XXX need to make this configureable
172 "enable_loop_devices" : True,
173 "enable_ccache" : True,
174 "enable_icecream" : False,
176 #self.settings.update(settings)
182 # Save the build time.
183 self
.build_time
= int(time
.time())
186 # Mount the directories.
192 # Setup domain name resolution in chroot.
195 # Extract all needed packages.
199 # Kill all still running processes.
200 util
.orphans_kill(self
.path
)
202 # Close pakfire instance.
205 # Umount the build environment.
214 Inherit architecture from distribution configuration.
216 return self
.distro
.arch
221 "build_date" : time
.strftime("%a, %d %b %Y %H:%M:%S +0000", time
.gmtime(self
.build_time
)),
222 "build_host" : socket
.gethostname(),
223 "build_id" : self
.build_id
,
224 "build_time" : self
.build_time
,
228 filename
= os
.path
.join(self
.path
, ".lock")
231 self
._lock
= open(filename
, "a+")
236 fcntl
.lockf(self
._lock
.fileno(), fcntl
.LOCK_EX | fcntl
.LOCK_NB
)
238 raise BuildRootLocked
, "Buildroot is locked"
247 def copyin(self
, file_out
, file_in
):
248 if file_in
.startswith("/"):
249 file_in
= file_in
[1:]
251 file_in
= self
.chrootPath(file_in
)
253 #if not os.path.exists(file_out):
256 dir_in
= os
.path
.dirname(file_in
)
257 if not os
.path
.exists(dir_in
):
260 self
.log
.debug("%s --> %s" % (file_out
, file_in
))
262 shutil
.copy2(file_out
, file_in
)
264 def copyout(self
, file_in
, file_out
):
265 if file_in
.startswith("/"):
266 file_in
= file_in
[1:]
268 file_in
= self
.chrootPath(file_in
)
270 #if not os.path.exists(file_in):
273 dir_out
= os
.path
.dirname(file_out
)
274 if not os
.path
.exists(dir_out
):
277 self
.log
.debug("%s --> %s" % (file_in
, file_out
))
279 shutil
.copy2(file_in
, file_out
)
281 def copy_result(self
, resultdir
):
282 dir_in
= self
.chrootPath("result")
284 for dir, subdirs
, files
in os
.walk(dir_in
):
285 basename
= os
.path
.basename(dir)
286 dir = dir[len(self
.chrootPath()):]
288 file_in
= os
.path
.join(dir, file)
290 file_out
= os
.path
.join(
296 self
.copyout(file_in
, file_out
)
298 def extract(self
, requires
=None, build_deps
=True):
300 Gets a dependency set and extracts all packages
306 if self
.use_cache
and os
.path
.exists(self
.cache_file
):
307 # If we are told to use the cache, we just import the
311 # Add neccessary build dependencies.
312 requires
+= BUILD_PACKAGES
314 # If we have ccache enabled, we need to extract it
315 # to the build chroot.
316 if self
.settings
.get("enable_ccache"):
317 requires
.append("ccache")
319 # If we have icecream enabled, we need to extract it
320 # to the build chroot.
321 if self
.settings
.get("enable_icecream"):
322 requires
.append("icecream")
324 # Get build dependencies from source package.
326 for req
in self
.pkg
.requires
:
329 # Install all packages.
330 self
.log
.info(_("Install packages needed for build..."))
331 self
.install(requires
)
333 # Copy the makefile and load source tarballs.
335 self
.pkg
.extract(_("Extracting"), prefix
=self
.build_dir
)
337 def install(self
, requires
):
339 Install everything that is required in requires.
341 # If we got nothing to do, we quit immediately.
346 self
.pakfire
.install(requires
, interactive
=False,
347 allow_downgrade
=True, logger
=self
.log
)
349 # Catch dependency errors and log it.
350 except DependencyError
, e
:
353 def install_test(self
):
355 for dir, subdirs
, files
in os
.walk(self
.chrootPath("result")):
357 pkgs
.append(os
.path
.join(dir, file))
359 self
.pakfire
.localinstall(pkgs
, yes
=True, allow_uninstall
=True)
361 def chrootPath(self
, *args
):
362 # Remove all leading slashes
365 if arg
.startswith("/"):
370 ret
= os
.path
.join(self
.path
, *args
)
371 ret
= ret
.replace("//", "/")
373 assert ret
.startswith(self
.path
)
377 def populate_dev(self
):
391 # If we need loop devices (which are optional) we create them here.
392 if self
.settings
["enable_loop_devices"]:
393 for i
in range(0, 7):
394 nodes
.append("/dev/loop%d" % i
)
397 # Stat the original node of the host system and copy it to
399 node_stat
= os
.stat(node
)
401 self
._create
_node
(node
, node_stat
.st_mode
, node_stat
.st_rdev
)
403 os
.symlink("/proc/self/fd/0", self
.chrootPath("dev", "stdin"))
404 os
.symlink("/proc/self/fd/1", self
.chrootPath("dev", "stdout"))
405 os
.symlink("/proc/self/fd/2", self
.chrootPath("dev", "stderr"))
406 os
.symlink("/proc/self/fd", self
.chrootPath("dev", "fd"))
410 Add DNS resolution facility to chroot environment by copying
411 /etc/resolv.conf and /etc/hosts.
413 for i
in ("/etc/resolv.conf", "/etc/hosts"):
416 def _create_node(self
, filename
, mode
, device
):
417 self
.log
.debug("Create node: %s (%s)" % (filename
, mode
))
419 filename
= self
.chrootPath(filename
)
421 # Create parent directory if it is missing.
422 dirname
= os
.path
.dirname(filename
)
423 if not os
.path
.exists(dirname
):
426 os
.mknod(filename
, mode
, device
)
429 self
.log
.debug("Destroying environment %s" % self
.path
)
431 if os
.path
.exists(self
.path
):
435 self
.log
.debug("Cleaning environemnt.")
437 # Remove the build directory and buildroot.
438 dirs
= (self
.build_dir
, self
.chrootPath("result"),)
441 if not os
.path
.exists(d
):
448 self
.log
.debug("Mounting environment")
449 for src
, dest
, fs
, options
in self
.mountpoints
:
450 mountpoint
= self
.chrootPath(dest
)
452 options
= "-o %s" % options
454 # Eventually create mountpoint directory
455 if not os
.path
.exists(mountpoint
):
456 os
.makedirs(mountpoint
)
458 cmd
= "mount -n -t %s %s %s %s" % \
459 (fs
, options
, src
, mountpoint
)
460 chroot
.do(cmd
, shell
=True)
462 def _umountall(self
):
463 self
.log
.debug("Umounting environment")
466 for src
, dest
, fs
, options
in reversed(self
.mountpoints
):
467 if not dest
in mountpoints
:
468 mountpoints
.append(dest
)
470 for dest
in mountpoints
:
471 mountpoint
= self
.chrootPath(dest
)
473 chroot
.do("umount -n %s" % mountpoint
, raiseExc
=0, shell
=True)
476 def mountpoints(self
):
479 # Make root as a tmpfs.
481 # ("pakfire_root", "/", "tmpfs", "defaults"),
485 # src, dest, fs, options
486 ("pakfire_proc", "/proc", "proc", "nosuid,noexec,nodev"),
487 ("/proc/sys", "/proc/sys", "bind", "bind"),
488 ("/proc/sys", "/proc/sys", "bind", "bind,ro,remount"),
489 ("/sys", "/sys", "bind", "bind"),
490 ("/sys", "/sys", "bind", "bind,ro,remount"),
491 ("pakfire_tmpfs", "/dev", "tmpfs", "mode=755,nosuid"),
492 ("/dev/pts", "/dev/pts", "bind", "bind"),
493 ("pakfire_tmpfs", "/run", "tmpfs", "mode=755,nosuid,nodev"),
496 # If selinux is enabled.
497 if os
.path
.exists("/sys/fs/selinux"):
499 ("/sys/fs/selinux", "/sys/fs/selinux", "bind", "bind"),
500 ("/sys/fs/selinux", "/sys/fs/selinux", "bind", "bind,ro,remount"),
503 # If ccache support is requested, we bind mount the cache.
504 if self
.settings
.get("enable_ccache"):
505 # Create ccache cache directory if it does not exist.
506 if not os
.path
.exists(CCACHE_CACHE_DIR
):
507 os
.makedirs(CCACHE_CACHE_DIR
)
510 (CCACHE_CACHE_DIR
, "/var/cache/ccache", "bind", "bind"),
518 # Add HOME manually, because it is occasionally not set
519 # and some builds get in trouble then.
521 "TERM" : os
.environ
.get("TERM", "dumb"),
524 # Set the container that we can detect, if we are inside a
526 "container" : "pakfire-builder",
529 # Inherit environment from distro
530 env
.update(self
.pakfire
.distro
.environ
)
532 # Icecream environment settings
533 if self
.settings
.get("enable_icecream", False):
534 # Set the toolchain path
535 if self
.settings
.get("icecream_toolchain", None):
536 env
["ICECC_VERSION"] = self
.settings
.get("icecream_toolchain")
538 # Set preferred host if configured.
539 if self
.settings
.get("icecream_preferred_host", None):
540 env
["ICECC_PREFERRED_HOST"] = \
541 self
.settings
.get("icecream_preferred_host")
543 # Fake UTS_MACHINE, when we cannot use the personality syscall and
544 # if the host architecture is not equal to the target architecture.
545 if not self
.pakfire
.distro
.personality
and \
546 not self
.pakfire
.config
.host_arch
== self
.pakfire
.distro
.arch
:
548 "LD_PRELOAD" : "/usr/lib/libpakfire_preload.so",
549 "UTS_MACHINE" : self
.pakfire
.distro
.arch
,
555 def installed_packages(self
):
557 Returns an iterator over all installed packages in this build environment.
559 # Get the repository of all installed packages.
560 repo
= self
.pakfire
.repos
.get_repo("@system")
562 # Return an iterator over the packages.
565 def do(self
, command
, shell
=True, personality
=None, logger
=None, *args
, **kwargs
):
568 # Environment variables
571 if kwargs
.has_key("env"):
572 env
.update(kwargs
.pop("env"))
574 self
.log
.debug("Environment:")
575 for k
, v
in sorted(env
.items()):
576 self
.log
.debug(" %s=%s" % (k
, v
))
578 # Update personality it none was set
580 personality
= self
.distro
.personality
582 # Make every shell to a login shell because we set a lot of
583 # environment things there.
585 command
= ["bash", "--login", "-c", command
]
587 if not kwargs
.has_key("chrootPath"):
588 kwargs
["chrootPath"] = self
.chrootPath()
592 personality
=personality
,
602 def build(self
, install_test
=True):
604 raise BuildError
, _("You cannot run a build when no package was given.")
606 # Search for the package file in build_dir and raise BuildError if it is not present.
607 pkgfile
= os
.path
.join(self
.build_dir
, "%s.%s" % (self
.pkg
.name
, MAKEFILE_EXTENSION
))
608 if not os
.path
.exists(pkgfile
):
609 raise BuildError
, _("Could not find makefile in build root: %s") % pkgfile
610 pkgfile
= "/%s" % os
.path
.relpath(pkgfile
, self
.chrootPath())
612 resultdir
= self
.chrootPath("/result")
614 # Create the build command, that is executed in the chroot.
615 build_command
= ["/usr/lib/pakfire/builder", "--offline",
616 "build", pkgfile
, "--arch", self
.arch
, "--nodeps",
617 "--resultdir=/result",]
620 self
.do(" ".join(build_command
), logger
=self
.log
)
623 raise BuildError
, _("The build command failed. See logfile for details.")
625 # Perform install test.
629 # Copy the final packages and stuff.
632 def shell(self
, args
=[]):
633 if not util
.cli_is_interactive():
634 self
.log
.warning("Cannot run shell on non-interactive console.")
637 # Install all packages that are needed to run a shell.
638 self
.install(SHELL_PACKAGES
)
640 # XXX need to set CFLAGS here
641 command
= "/usr/sbin/chroot %s %s %s" % \
642 (self
.chrootPath(), SHELL_SCRIPT
, " ".join(args
))
644 # Add personality if we require one
645 if self
.pakfire
.distro
.personality
:
646 command
= "%s %s" % (self
.pakfire
.distro
.personality
, command
)
648 for key
, val
in self
.environ
.items():
649 command
= "%s=\"%s\" " % (key
, val
) + command
651 # Empty the environment
652 command
= "env -i - %s" % command
654 self
.log
.debug("Shell command: %s" % command
)
656 shell
= os
.system(command
)
657 return os
.WEXITSTATUS(shell
)
660 def cache_file(self
):
662 self
.pakfire
.distro
.sname
, # name of the distribution
663 self
.pakfire
.distro
.release
, # release version
664 self
.pakfire
.distro
.arch
, # architecture
667 return os
.path
.join(CACHE_ENVIRON_DIR
, "%s.cache" %"-".join(comps
))
669 def cache_export(self
, filename
):
670 # Sync all disk caches.
673 # A list to store all mountpoints, so we don't package them.
676 # A list containing all files we want to package.
679 # Walk through the whole tree and collect all files
680 # that are on the same disk (not crossing mountpoints).
681 log
.info(_("Creating filelist..."))
682 root
= self
.chrootPath()
683 for dir, subdirs
, files
in os
.walk(root
):
684 # Search for mountpoints and skip them.
685 if not dir == root
and os
.path
.ismount(dir):
686 mountpoints
.append(dir)
689 # Skip all directories under mountpoints.
690 if any([dir.startswith(m
) for m
in mountpoints
]):
693 # Add all other files.
696 file = os
.path
.join(dir, file)
697 filelist
.append(file)
699 # Create a nice progressbar.
700 p
= util
.make_progress(_("Compressing files..."), len(filelist
))
703 # Create tar file and add all files to it.
704 f
= packages
.file.InnerTarFile
.open(filename
, "w:gz")
705 for file in filelist
:
710 f
.add(file, os
.path
.relpath(file, root
), recursive
=False)
713 # Finish progressbar.
717 filesize
= os
.path
.getsize(filename
)
719 log
.info(_("Cache file was successfully created at %s.") % filename
)
720 log
.info(_(" Containing %(files)s files, it has a size of %(size)s.") % \
721 { "files" : len(filelist
), "size" : util
.format_size(filesize
), })
723 def cache_extract(self
):
724 root
= self
.chrootPath()
725 filename
= self
.cache_file
727 f
= packages
.file.InnerTarFile
.open(filename
, "r:gz")
728 members
= f
.getmembers()
730 # Make a nice progress bar as always.
731 p
= util
.make_progress(_("Extracting files..."), len(members
))
733 # Extract all files from the cache.
735 for member
in members
:
740 f
.extract(member
, path
=root
)
743 # Finish progressbar.
747 # Re-read local repository.
748 self
.pakfire
.repos
.local
.update(force
=True)
750 # Update all packages.
751 self
.log
.info(_("Updating packages from cache..."))
752 self
.pakfire
.update(interactive
=False, logger
=self
.log
,
753 allow_archchange
=True, allow_vendorchange
=True, allow_downgrade
=True)
756 class Builder(object):
757 def __init__(self
, pakfire
, filename
, resultdir
, **kwargs
):
758 self
.pakfire
= pakfire
760 self
.filename
= filename
762 self
.resultdir
= resultdir
765 self
.pkg
= packages
.Makefile(self
.pakfire
, self
.filename
)
773 Create a temporary file in the build environment.
775 file = "/tmp/pakfire_%s" % util
.random_string()
785 return self
.pkg
.buildroot
789 return self
.pakfire
.distro
795 # Get all definitions from the package.
796 environ
.update(self
.pkg
.exports
)
798 # Overwrite some definitions by default values.
799 environ
.update(self
._environ
)
803 def do(self
, command
, shell
=True, personality
=None, cwd
=None, *args
, **kwargs
):
804 # Environment variables
805 log
.debug("Environment:")
806 for k
, v
in sorted(self
.environ
.items()):
807 log
.debug(" %s=%s" % (k
, v
))
809 # Update personality it none was set
811 personality
= self
.distro
.personality
814 cwd
= "/%s" % LOCAL_TMP_PATH
816 # Make every shell to a login shell because we set a lot of
817 # environment things there.
819 command
= ["bash", "--login", "-c", command
]
823 personality
=personality
,
826 logger
=logging
.getLogger("pakfire"),
832 def create_icecream_toolchain(self
):
834 out
= self
.do("icecc --build-native 2>/dev/null", returnOutput
=True, cwd
="/tmp")
838 for line
in out
.splitlines():
839 m
= re
.match(r
"^creating ([a-z0-9]+\.tar\.gz)", line
)
841 self
._environ
["ICECC_VERSION"] = "/tmp/%s" % m
.group(1)
843 def create_buildscript(self
, stage
):
844 file = "/tmp/build_%s" % util
.random_string()
846 # Get buildscript from the package.
847 script
= self
.pkg
.get_buildscript(stage
)
849 # Write script to an empty file.
851 f
.write("#!/bin/sh\n\n")
854 f
.write("\n%s\n" % script
)
862 # Create buildroot and remove all content if it was existant.
863 util
.rm(self
.buildroot
)
864 os
.makedirs(self
.buildroot
)
866 # Build icecream toolchain if icecream is installed.
867 self
.create_icecream_toolchain()
869 for stage
in ("prepare", "build", "test", "install"):
870 self
.build_stage(stage
)
872 # Run post-build stuff.
873 self
.post_compress_man_pages()
874 self
.post_remove_static_libs()
875 self
.post_extract_debuginfo()
877 # Package the result.
878 # Make all these little package from the build environment.
879 log
.info(_("Creating packages:"))
881 for pkg
in reversed(self
.pkg
.packages
):
882 packager
= packages
.packager
.BinaryPackager(self
.pakfire
, pkg
,
883 self
, self
.buildroot
)
884 pkg
= packager
.run(self
.resultdir
)
888 for pkg
in sorted(pkgs
):
889 for line
in pkg
.dump(long=True).splitlines():
894 def build_stage(self
, stage
):
895 # Get the buildscript for this stage.
896 buildscript
= self
.create_buildscript(stage
)
898 # Execute the buildscript of this stage.
899 log
.info(_("Running stage %s:") % stage
)
902 self
.do(buildscript
, shell
=False)
905 # Remove the buildscript.
906 if os
.path
.exists(buildscript
):
907 os
.unlink(buildscript
)
909 def post_remove_static_libs(self
):
910 keep_libs
= self
.pkg
.lexer
.build
.get_var("keep_libraries")
911 keep_libs
= keep_libs
.split()
914 self
.do("%s/remove-static-libs %s %s" % \
915 (SCRIPT_DIR
, self
.buildroot
, " ".join(keep_libs
)))
917 log
.warning(_("Could not remove static libraries: %s") % e
)
919 def post_compress_man_pages(self
):
921 self
.do("%s/compress-man-pages %s" % (SCRIPT_DIR
, self
.buildroot
))
923 log
.warning(_("Compressing man pages did not complete successfully."))
925 def post_extract_debuginfo(self
):
928 # Check if we need to run with strict build-id.
929 strict_id
= self
.pkg
.lexer
.build
.get_var("debuginfo_strict_build_id", "true")
930 if strict_id
in ("true", "yes", "1"):
931 args
.append("--strict-build-id")
933 args
.append("--buildroot=%s" % self
.pkg
.buildroot
)
934 args
.append("--sourcedir=%s" % self
.pkg
.sourcedir
)
936 # Get additional options to pass to script.
937 options
= self
.pkg
.lexer
.build
.get_var("debuginfo_options", "")
938 args
+= options
.split()
941 self
.do("%s/extract-debuginfo %s %s" % (SCRIPT_DIR
, " ".join(args
), self
.pkg
.buildroot
))
943 log
.error(_("Extracting debuginfo did not complete with success. Aborting build."))
947 if os
.path
.exists(self
.buildroot
):
948 util
.rm(self
.buildroot
)