diff options
-rw-r--r-- | .gitignore | 14 | ||||
-rw-r--r-- | LICENSE | 502 | ||||
-rw-r--r-- | MANIFEST.in | 1 | ||||
l--------- | README | 1 | ||||
-rw-r--r-- | README.md | 246 | ||||
-rw-r--r-- | debian/changelog (renamed from changelog) | 0 | ||||
-rw-r--r-- | debian/clean (renamed from clean) | 0 | ||||
-rw-r--r-- | debian/compat (renamed from compat) | 0 | ||||
-rw-r--r-- | debian/control (renamed from control) | 0 | ||||
-rw-r--r-- | debian/copyright (renamed from copyright) | 0 | ||||
-rw-r--r-- | debian/docs (renamed from docs) | 0 | ||||
-rw-r--r-- | debian/gbp.conf (renamed from gbp.conf) | 0 | ||||
-rw-r--r-- | debian/manpages (renamed from manpages) | 0 | ||||
-rwxr-xr-x | debian/rules (renamed from rules) | 0 | ||||
-rw-r--r-- | debian/source/format (renamed from source/format) | 0 | ||||
-rw-r--r-- | debian/source/options (renamed from source/options) | 0 | ||||
-rw-r--r-- | debian/watch (renamed from watch) | 0 | ||||
-rwxr-xr-x | mkosi | 1657 | ||||
-rwxr-xr-x | setup.py | 14 |
19 files changed, 2435 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a241340 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +/SHA256SUM +/SHA256SUM.gpg +/__pycache__ +/mkosi.egg-info +/build +/dist +/image +/image.raw +/image.raw.xz +/image.tar.xz +/mkosi.build +/mkosi.default +/mkosi.extra +/mkosi.nspawn @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..1aba38f --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include LICENSE @@ -0,0 +1 @@ +README.md
\ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..41c146b --- /dev/null +++ b/README.md @@ -0,0 +1,246 @@ +# mkosi - Create legacy-free OS images + +A fancy wrapper around `dnf --installroot`, `debootstrap` and +`pacstrap`, that may generate disk images with a number of +bells and whistles. + +# Supported output formats + +The following output formats are supported: + +* Raw *GPT* disk image, with ext4 as root (*raw_gpt*) + +* Raw *GPT* disk image, with btrfs as root (*raw_btrfs*) + +* Plain directory, containing the *OS* tree (*directory*) + +* btrfs subvolume, with separate subvolumes for `/var`, `/home`, + `/srv`, `/var/tmp` (*subvolume*) + +* Tarball (*tar*) + +When a *GPT* disk image is created, the following additional +options are available: + +* A swap partition may be added in + +* The image may be made bootable on *EFI* systems + +* Separate partitions for `/srv` and `/home` may be added in + +# Compatibility + +Generated images are *legacy-free*. This means only *GPT* disk +labels (and no *MBR* disk labels) are supported, and only +systemd based images may be generated. Moreover, for bootable +images only *EFI* systems are supported (not plain *MBR/BIOS*). + +Currently, the *EFI* boot loader does not support *SecureBoot*, +and hence cannot generate signed *SecureBoot* images. + +All generated *GPT* disk images may be booted in a local +container directly with: + +```bash +systemd-nspawn -bi image.raw +``` + +Additionally, bootable *GPT* disk images (as created with the +`--bootable` flag) work when booted directly by *EFI* systems, for +example in *KVM* via: + +```bash +qemu-kvm -m 512 -smp 2 -bios /usr/share/edk2/ovmf/OVMF_CODE.fd -drive format=raw,file=image.raw +``` + +*EFI* bootable *GPT* images are larger than plain *GPT* images, as +they additionally carry an *EFI* system partition containing a +boot loader, as well as a kernel, kernel modules, udev and +more. + +All directory or btrfs subvolume images may be booted directly +with: + +```bash +systemd-nspawn -bD image +``` + +# Other features + +* Optionally, create an *SHA256SUM* checksum file for the result, + possibly even signed via gpg. + +* Optionally, place a specific `.nspawn` settings file along + with the result. + +* Optionally, build a local project's *source* tree in the image + and add the result to the generated image (see below). + +* Optionally, share *RPM*/*DEB* package cache between multiple runs, + in order to optimize build speeds. + +* Optionally, the resulting image may be compressed with *XZ*. + +* Optionally, btrfs' read-only flag for the root subvolume may be + set. + +* Optionally, btrfs' compression may be enabled for all + created subvolumes. + +* By default images are created without all files marked as + documentation in the packages, on distributions where the + package manager supports this. Use the `--with-docs` flag to + build an image with docs added. + +# Supported distributions + +Images may be created containing installations of the +following *OS*es. + +* *Fedora* + +* *Debian* + +* *Ubuntu* + +* *Arch Linux* (incomplete) + +In theory, any distribution may be used on the host for +building images containing any other distribution, as long as +the necessary tools are available. Specifically, any distro +that packages `debootstrap` may be used to build *Debian* or +*Ubuntu* images. Any distro that packages `dnf` may be used to +build *Fedora* images. Any distro that packages `pacstrap` may +be used to build *Arch Linux* images. + +Currently, *Fedora* packages all three tools. + +# Files + +To make it easy to build images for development versions of +your projects, mkosi can read configuration data from the +local directory, under the assumption that it is invoked from +a *source* tree. Specifically, the following files are used if +they exist in the local directory: + +* `mkosi.default` may be used to configure mkosi's image + building process. For example, you may configure the + distribution to use (`fedora`, `ubuntu`, `debian`, `archlinux`) for + the image, or additional distribution packages to + install. Note that all options encoded in this configuration + file may also be set on the command line, and this file is + hence little more than a way to make sure simply typing + `mkosi` without further parameters in your *source* tree is + enough to get the right image of your choice set up. + +* `mkosi.extra` may be a directory. If this exists all files + contained in it are copied over the directory tree of the + image after the *OS* was installed. This may be used to add in + additional files to an image, on top of what the + distribution includes in its packages. + +* `mkosi.build` may be an executable script. If it exists the + image will be built twice: the first iteration will be the + *development* image, the second iteration will be the + *final* image. The *development* image is used to build the + project in the current working directory (the *source* + tree). For that the whole directory is copied into the + image, along with the mkosi.build build script. The script + is then invoked inside the image (via `systemd-nspawn`), with + `$SRCDIR` pointing to the *source* tree. `$DESTDIR` points to a + directory where the script should place any files generated + it would like to end up in the *final* image. Note that + `make`/`automake` based build systems generally honour `$DESTDIR`, + thus making it very natural to build *source* trees from the + build script. After the *development* image was built and the + build script ran inside of it, it is removed again. After + that the *final* image is built, without any *source* tree or + build script copied in. However, this time the contents of + `$DESTDIR` is added into the image. + +* `mkosi.nspawn` may be an nspawn settings file. If this exists + it will be copied into the same place as the output image + file. This is useful since nspawn looks for settings files + next to image files it boots, for additional container + runtime settings. + +All these files are optional. + +Note that the location of all these files may also be +configured during invocation via command line switches, and as +settings in `mkosi.default`, in case the default settings are +not acceptable for a project. + +# Examples + +Create and run a raw *GPT* image with *ext4*, as `image.raw`: + +```bash +# mkosi +# systemd-nspawn -b -i image.raw +``` + +Create and run a bootable btrfs *GPT* image, as `foobar.raw`: + +```bash +# mkosi -t raw_btrfs --bootable -o foobar.raw +# systemd-nspawn -b -i foobar.raw +# qemu-kvm -m 512 -smp 2 -bios /usr/share/edk2/ovmf/OVMF_CODE.fd -drive format=raw,file=foobar.raw +``` + +Create and run a *Fedora* image into a plain directory: + +```bash +# mkosi -d fedora -t directory -o quux +# systemd-nspawn -b quux +``` + +Create a compressed tar ball `image.raw.xz` and add a checksum +file, and install *SSH* into it: + +```bash +# mkosi -d fedora -t tar --checksum --compress --package=openssh-clients +``` + +Inside the source directory of an `automake`-based project, +configure *mkosi* so that simply invoking `mkosi` without any +parameters builds an *OS* image containing a built version of +the project in its current state: + +```bash +# cat > mkosi.default <<EOF +[Distribution] +Distribution=fedora +Release=24 + +[Output] +Format=raw_btrfs +Bootable=yes + +[Packages] +Packages=openssh-clients httpd +BuildPackages=make gcc libcurl-devel +EOF +# cat > mkosi.build <<EOF +#!/bin/sh +cd $SRCDIR <<EOF +./autogen.sh +./configure --prefix=/usr +make -j `nproc` +make install +EOF +# chmod +x mkosi.build +# mkosi +# systemd-nspawn -bi image.raw +``` + +# Requirements + +mkosi is packaged for various distributions: Debian, Ubuntu, Arch (in AUR), Fedora. +It is usually easiest to use the distribution package. + +When not using distribution packages, for example, on *Fedora* you need: + +```bash +dnf install python3 debootstrap arch-install-scripts xz btrfs-progs dosfstools edk2-ovmf +``` diff --git a/changelog b/debian/changelog index d5e0660..d5e0660 100644 --- a/changelog +++ b/debian/changelog diff --git a/copyright b/debian/copyright index f614ba3..f614ba3 100644 --- a/copyright +++ b/debian/copyright diff --git a/gbp.conf b/debian/gbp.conf index 6dc3643..6dc3643 100644 --- a/gbp.conf +++ b/debian/gbp.conf diff --git a/manpages b/debian/manpages index 7bd2ebd..7bd2ebd 100644 --- a/manpages +++ b/debian/manpages diff --git a/source/format b/debian/source/format index 163aaf8..163aaf8 100644 --- a/source/format +++ b/debian/source/format diff --git a/source/options b/debian/source/options index 50032e0..50032e0 100644 --- a/source/options +++ b/debian/source/options @@ -0,0 +1,1657 @@ +#!/usr/bin/python3 + +import argparse +import configparser +import contextlib +import ctypes, ctypes.util +import crypt +import hashlib +import os +import platform +import shutil +import subprocess +import sys +import tempfile +import time +import uuid +from enum import Enum + +__version__ = '1' + +# TODO +# - squashfs root +# - volatile images +# - make debian/ubuntu images bootable +# - work on device nodes +# - allow passing env vars +# - rework cache management to use mkosi.cache by default in the project dir + +class OutputFormat(Enum): + raw_gpt = 1 + raw_btrfs = 2 + directory = 3 + subvolume = 4 + tar = 5 + +class Distribution(Enum): + fedora = 1 + debian = 2 + ubuntu = 3 + arch = 4 + +GPT_ROOT_X86 = uuid.UUID("44479540f29741b29af7d131d5f0458a") +GPT_ROOT_X86_64 = uuid.UUID("4f68bce3e8cd4db196e7fbcaf984b709") +GPT_ROOT_ARM = uuid.UUID("69dad7102ce44e3cb16c21a1d49abed3") +GPT_ROOT_ARM_64 = uuid.UUID("b921b0451df041c3af444c6f280d3fae") +GPT_ROOT_IA64 = uuid.UUID("993d8d3df80e4225855a9daf8ed7ea97") +GPT_ESP = uuid.UUID("c12a7328f81f11d2ba4b00a0c93ec93b") +GPT_SWAP = uuid.UUID("0657fd6da4ab43c484e50933c84b4f4f") +GPT_HOME = uuid.UUID("933ac7e12eb44f13b8440e14e2aef915") +GPT_SRV = uuid.UUID("3b8f842520e04f3b907f1a25a76f98e8") + +if platform.machine() == "x86_64": + GPT_ROOT_NATIVE = GPT_ROOT_X86_64 +elif platform.machine() == "aarch64": + GPT_ROOT_NATIVE = GPT_ROOT_ARM_64 +else: + sys.stderr.write("Don't known the %s architecture.\n" % platform.machine()) + sys.exit(1) + +CLONE_NEWNS = 0x00020000 + +def unshare(flags): + libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True) + + if libc.unshare(ctypes.c_int(flags)) != 0: + e = ctypes.get_errno() + raise OSError(e, os.strerror(e)) + +def init_namespace(args): + print_step("Detaching namespace...") + + args.original_umask = os.umask(0o000) + unshare(CLONE_NEWNS) + + subprocess.run(["mount", "--make-rslave", "/"], check=True) + + print_step("Detaching namespace complete.") + +def print_step(text): + sys.stderr.write("‣ \033[0;1;39m" + text + "\033[0m\n") + +def setup_workspace(args): + print_step("Setting up temporary workspace.") + if args.output_format in (OutputFormat.directory, OutputFormat.subvolume): + d = tempfile.TemporaryDirectory(dir=os.path.dirname(args.output), prefix='.mkosi-') + else: + d = tempfile.TemporaryDirectory(dir='/var/tmp', prefix='mkosi-') + + print_step("Temporary workspace in " + d.name + " is now set up.") + return d + +def btrfs_subvol_create(path, mode=0o755): + m = os.umask(~mode & 0o7777) + subprocess.run(["btrfs", "subvol", "create", path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True) + os.umask(m) + +def btrfs_subvol_delete(path, mode=0o755): + subprocess.run(["btrfs", "subvol", "delete", path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True) + +def btrfs_subvol_make_ro(path, b=True): + subprocess.run(["btrfs", "property", "set", path, "ro", "true" if b else "false"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True) + +def image_size(args): + size = args.root_size + + if args.home_size is not None: + size += args.home_size + if args.srv_size is not None: + size += args.srv_size + if args.bootable: + size += args.esp_size + if args.swap_size is not None: + size += args.swap_size + + return size + +def create_image(args, workspace): + if not args.output_format in (OutputFormat.raw_gpt, OutputFormat.raw_btrfs): + return None + + print_step("Creating partition table...") + + f = tempfile.NamedTemporaryFile(dir = os.path.dirname(args.output), prefix='.mkosi-') + subprocess.run(["chattr", "+C", f.name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True) + f.truncate(image_size(args)) + + pn = 1 + table = "label: gpt\n" + + if args.bootable: + table += 'size={}, type={}, name="ESP System Partition"\n'.format(str(int(args.esp_size / 512)), GPT_ESP) + args.esp_partno = pn + pn += 1 + else: + args.esp_partno = None + + if args.swap_size is not None: + table += 'size={}, type={}, name="Swap Partition"\n'.format(str(int(args.swap_size / 512)), GPT_SWAP) + args.swap_partno = pn + pn += 1 + else: + args.swap_partno = None + + args.home_partno = None + args.srv_partno = None + + if args.output_format != OutputFormat.raw_btrfs: + if args.home_size is not None: + table += 'size={}, type={}, name="Home Partition"\n'.format(str(int(args.home_size / 512)), GPT_HOME) + args.home_partno = pn + pn += 1 + + if args.srv_size is not None: + table += 'size={}, type={}, name="Server Data Partition"\n'.format(str(int(args.srv_size / 512)), GPT_SRV) + args.srv_partno = pn + pn += 1 + + table += 'type={}, name="Root Partition"\n'.format(GPT_ROOT_NATIVE) + + args.root_partno = pn + + pn += 1 + + subprocess.run(["sfdisk", "--color=never", f.name], input=table.encode("utf-8"), check=True) + subprocess.run(["sync"]) + + print_step("Created partition table as " + f.name + ".") + + return f + +@contextlib.contextmanager +def attach_image_loopback(args, raw): + if raw is None: + yield None + return + + print_step("Attaching image file...") + c = subprocess.run(["losetup", "--find", "--show", "--partscan", raw.name], + stdout=subprocess.PIPE, check=True) + loopdev = c.stdout.decode("utf-8").strip() + print_step("Attached image file as " + loopdev + ".") + + try: + yield loopdev + finally: + print_step("Detaching image file..."); + subprocess.run(["losetup", "--detach", loopdev], check=True) + print_step("Detaching image file completed."); + +def partition(loopdev, partno): + return loopdev + "p" + str(partno) + +def prepare_swap(args, loopdev): + if loopdev is None: + return + + if args.swap_partno is None: + return + + print_step("Formatting swap partition..."); + + subprocess.run(["mkswap", "-Lswap", partition(loopdev, args.swap_partno)], check=True) + + print_step("Formatting swap partition completed."); + +def prepare_esp(args, loopdev): + if loopdev is None: + return + if args.esp_partno is None: + return + + print_step("Formatting ESP partition..."); + + subprocess.run(["mkfs.fat", "-nEFI", "-F32", partition(loopdev, args.esp_partno)], check=True) + + print_step("Formatting ESP partition completed."); + +def mkfs_ext4(label, mount, loopdev, partno): + subprocess.run(["mkfs.ext4", "-L", label, "-M", mount, partition(loopdev, partno)], check=True) + +def prepare_root(args, loopdev): + if loopdev is None: + return + if args.root_partno is None: + return + + print_step("Formatting root partition..."); + + if args.output_format == OutputFormat.raw_btrfs: + subprocess.run(["mkfs.btrfs", "-Lroot", partition(loopdev, args.root_partno)], check=True) + else: + mkfs_ext4("root", "/", loopdev, args.root_partno) + + print_step("Formatting root partition completed."); + +def prepare_home(args, loopdev): + if loopdev is None: + return + if args.home_partno is None: + return + + print_step("Formatting home partition..."); + + mkfs_ext4("home", "/home", loopdev, args.home_partno) + + print_step("Formatting home partition completed."); + +def prepare_srv(args, loopdev): + if loopdev is None: + return + if args.srv_partno is None: + return + + print_step("Formatting server data partition..."); + + mkfs_ext4("srv", "/srv", loopdev, args.srv_partno) + + print_step("Formatted server data partition."); + +def mount_loop(args, loopdev, partno, where): + os.makedirs(where, 0o755, True) + + options = "-odiscard" + + if args.compress and args.output_format == OutputFormat.raw_btrfs: + options += ",compress" + + subprocess.run(["mount", "-n", partition(loopdev, partno), where, options], check=True) + +def mount_bind(what, where): + os.makedirs(where, 0o755, True) + subprocess.run(["mount", "--bind", what, where], check=True) + +@contextlib.contextmanager +def mount_image(args, workspace, loopdev): + if loopdev is None: + yield None + return + + print_step("Mounting image..."); + + root = os.path.join(workspace, "root") + mount_loop(args, loopdev, args.root_partno, root) + + if args.home_partno is not None: + mount_loop(args, loopdev, args.home_partno, os.path.join(root, "home")) + + if args.srv_partno is not None: + mount_loop(args, loopdev, args.srv_partno, os.path.join(root, "srv")) + + if args.esp_partno is not None: + mount_loop(args, loopdev, args.esp_partno, os.path.join(root, "boot/efi")) + + if args.distribution == Distribution.fedora: + mount_bind("/proc", os.path.join(root, "proc")) + mount_bind("/dev", os.path.join(root, "dev")) + mount_bind("/sys", os.path.join(root, "sys")) + + print_step("Mounting image completed."); + try: + yield + finally: + print_step("Unmounting image..."); + + umount(os.path.join(root, "home")) + umount(os.path.join(root, "srv")) + umount(os.path.join(root, "boot/efi")) + umount(os.path.join(root, "proc")) + umount(os.path.join(root, "sys")) + umount(os.path.join(root, "dev")) + umount(os.path.join(root, "var/cache/dnf")) + umount(os.path.join(root, "var/cache/apt/archives")) + umount(os.path.join(root)) + + print_step("Unmounting image completed."); + +def mount_cache(args, workspace): + if not args.distribution in (Distribution.fedora, Distribution.debian, Distribution.ubuntu): + return + + if args.cache_path is None: + return + + # We can't do this in mount_image() yet, as /var itself might have to be created as a subvolume first + if args.distribution == Distribution.fedora: + mount_bind(args.cache_path, os.path.join(workspace, "root", "var/cache/dnf")) + elif args.distribution in (Distribution.debian, Distribution.ubuntu): + mount_bind(args.cache_path, os.path.join(workspace, "root", "var/cache/apt/archives")) + +def umount(where): + # Ignore failures and error messages + subprocess.run(["umount", "-n", where], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + +def prepare_tree(args, workspace): + print_step("Setting up basic OS tree..."); + + if args.output_format == OutputFormat.subvolume: + btrfs_subvol_create(os.path.join(workspace, "root")) + else: + try: + os.mkdir(os.path.join(workspace, "root")) + except FileExistsError: + pass + + if args.output_format in (OutputFormat.subvolume, OutputFormat.raw_btrfs): + btrfs_subvol_create(os.path.join(workspace, "root", "home")) + btrfs_subvol_create(os.path.join(workspace, "root", "srv")) + btrfs_subvol_create(os.path.join(workspace, "root", "var")) + btrfs_subvol_create(os.path.join(workspace, "root", "var/tmp"), 0o1777) + os.mkdir(os.path.join(workspace, "root", "var/lib")) + btrfs_subvol_create(os.path.join(workspace, "root", "var/lib/machines"), 0o700) + + if args.bootable: + # We need an initialized machine ID for the boot logic to work + mid = uuid.uuid4().hex + os.mkdir(os.path.join(workspace, "root", "etc"), 0o755) + open(os.path.join(workspace, "root", "etc/machine-id"), "w").write(mid + "\n") + + # For now, let's stay compatible with traditional Linux ESP mounts + os.mkdir(os.path.join(workspace, "root", "boot/efi/EFI"), 0o700) + os.mkdir(os.path.join(workspace, "root", "boot/efi/EFI/BOOT"), 0o700) + os.mkdir(os.path.join(workspace, "root", "boot/efi/EFI/systemd"), 0o700) + os.mkdir(os.path.join(workspace, "root", "boot/efi/loader"), 0o700) + os.mkdir(os.path.join(workspace, "root", "boot/efi/loader/entries"), 0o700) + os.mkdir(os.path.join(workspace, "root", "boot/efi", mid), 0o700) + + os.symlink("efi/loader", os.path.join(workspace, "root", "boot/loader")) + os.symlink("efi/" + mid, os.path.join(workspace, "root", "boot", mid)) + + os.mkdir(os.path.join(workspace, "root", "etc/kernel"), 0o755) + + with open(os.path.join(workspace, "root", "etc/kernel/cmdline"), "w") as cmdline: + cmdline.write(args.kernel_commandline) + cmdline.write("\n") + + print_step("Setting up basic OS tree completed."); + +def patch_file(filepath, line_rewriter): + temp_new_filepath = filepath + ".tmp.new" + + with open(filepath, "r") as old: + with open(temp_new_filepath, "w") as new: + for line in old: + new.write(line_rewriter(line)) + + shutil.copystat(filepath, temp_new_filepath) + os.remove(filepath) + shutil.move(temp_new_filepath, filepath) + +def enable_networkd(workspace): + subprocess.run(["systemctl", + "--root", os.path.join(workspace, "root"), + "enable", "systemd-networkd", "systemd-resolved"], + check=True) + + os.remove(os.path.join(workspace, "root", "etc/resolv.conf")) + os.symlink("../usr/lib/systemd/resolv.conf", os.path.join(workspace, "root", "etc/resolv.conf")) + + patch_file(os.path.join(workspace, "root", "etc/nsswitch.conf"), + lambda line: " ".join(["resolve" if w == "dns" else w for w in line.split(" ")]) if line.startswith("hosts:") else line) + + with open(os.path.join(workspace, "root", "etc/systemd/network/all-ethernet.network"), "w") as f: + f.write("""\ +[Match] +Type=ether + +[Network] +DHCP=yes +""") + +def run_workspace_command(workspace, *cmd, network=False): + cmdline = ["systemd-nspawn", + '--quiet', + "--directory", os.path.join(workspace, "root"), + "--as-pid2", + "--register=no"] + if not network: + cmdline += ["--private-network"] + + cmdline += ['--', *cmd] + subprocess.run(cmdline, check=True) + +def install_fedora(args, workspace, run_build_script): + print_step("Installing Fedora...") + + gpg_key = "/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-%s-x86_64" % args.release + if os.path.exists(gpg_key): + gpg_key = "file://%s" % gpg_key + else: + gpg_key = "https://getfedora.org/static/81B46521.txt" + + if args.mirror: + release_url = "baseurl={.mirror}/releases/{.release}/Everything/x86_64/os/".format(args) + updates_url = "baseurl={.mirror}/updates/{.release}/x86_64/".format(args) + else: + release_url = ("metalink=https://mirrors.fedoraproject.org/metalink?" + + "repo=fedora-{.release}&arch=x86_64".format(args)) + updates_url = ("metalink=https://mirrors.fedoraproject.org/metalink?" + + "repo=updates-released-f{.release}&arch=x86_64".format(args)) + + with open(os.path.join(workspace, "dnf.conf"), "w") as f: + f.write("""\ +[main] +gpgcheck=1 + +[fedora] +name=Fedora {args.release} - base +{release_url} +gpgkey={gpg_key} + +[updates] +name=Fedora {args.release} - updates +{updates_url} +gpgkey={gpg_key} +""".format(args=args, + gpg_key=gpg_key, + release_url=release_url, + updates_url=updates_url)) + + root = os.path.join(workspace, "root") + cmdline = ["dnf", + "-y", + "--config=" + os.path.join(workspace, "dnf.conf"), + "--best", + "--allowerasing", + "--releasever=" + args.release, + "--installroot=" + root, + "--disablerepo=*", + "--enablerepo=fedora", + "--enablerepo=updates", + "--setopt=keepcache=1", + "--setopt=install_weak_deps=0"] + + # Turn off docs, but not during the development build, as dnf currently has problems with that + if not args.with_docs and not run_build_script: + cmdline.append("--setopt=tsflags=nodocs") + + cmdline.extend([ + "install", + "systemd", + "fedora-release", + "passwd"]) + + if args.packages is not None: + cmdline.extend(args.packages) + + if run_build_script and args.build_packages is not None: + cmdline.extend(args.build_packages) + + if args.bootable: + cmdline.extend(["kernel", "systemd-udev"]) + os.makedirs(os.path.join(root, 'efi'), exist_ok=True) + + subprocess.run(cmdline, check=True) + + print_step("Installing Fedora completed.") + +def install_debian_or_ubuntu(args, workspace, run_build_script, mirror): + cmdline = ["debootstrap", + "--verbose", + "--variant=minbase", + "--include=systemd-sysv", + "--exclude=sysv-rc,initscripts,startpar,lsb-base,insserv", + args.release, + workspace + "/root", + mirror] + if args.bootable and args.output_format == OutputFormat.raw_btrfs: + cmdline[3] += ",btrfs-tools" + + subprocess.run(cmdline, check=True) + + + # Debootstrap is not smart enough to deal correctly with alternative dependencies + # Installing libpam-systemd via debootstrap results in systemd-shim being installed + # Therefore, prefer to install via apt from inside the container + extra_packages = [ 'dbus', 'libpam-systemd'] + + # Also install extra packages via the secondary APT run, because it is smarter and + # can deal better with any conflicts + if args.packages is not None: + extra_packages += args.packages + + if run_build_script and args.build_packages is not None: + extra_packages += args.build_packages + + # Work around debian bug #835628 + os.makedirs(os.path.join(workspace, "root/etc/dracut.conf.d"), exist_ok=True) + with open(os.path.join(workspace, "root/etc/dracut.conf.d/99-generic.conf"), "w") as f: + f.write("hostonly=no") + + if args.bootable: + extra_packages += ["linux-image-amd64", "dracut"] + + if extra_packages: + # Debian policy is to start daemons by default. + # The policy-rc.d script can be used choose which ones to start + # Let's install one that denies all daemon startups + # See https://people.debian.org/~hmh/invokerc.d-policyrc.d-specification.txt + # Note: despite writing in /usr/sbin, this file is not shipped by the OS + # and instead should be managed by the admin. + policyrcd = os.path.join(workspace, "root/usr/sbin/policy-rc.d") + with open(policyrcd, "w") as f: + f.write("#!/bin/sh\n") + f.write("exit 101") + os.chmod(policyrcd, 0o755) + cmdline = ["/usr/bin/apt-get", "--assume-yes", "--no-install-recommends", "install"] + extra_packages + run_workspace_command(workspace, network=True, *cmdline) + os.unlink(policyrcd) + +def install_debian(args, workspace, run_build_script): + print_step("Installing Debian...") + + install_debian_or_ubuntu(args, workspace, run_build_script, args.mirror) + + print_step("Installing Debian completed.") + +def install_ubuntu(args, workspace, run_build_script): + print_step("Installing Ubuntu...") + + install_debian_or_ubuntu(args, workspace, run_build_script, args.mirror) + + print_step("Installing Ubuntu completed.") + +def install_arch(args, workspace, run_build_script): + if args.release is not None: + sys.stderr.write("Distribution release specification is not supported for ArchLinux, ignoring.") + + print_step("Installing ArchLinux...") + + keyring = "archlinux" + + if platform.machine() == "aarch64": + keyring += "arm" + + subprocess.run(["pacman-key", "--nocolor", "--init"], check=True) + subprocess.run(["pacman-key", "--nocolor", "--populate", keyring], check=True) + + + if platform.machine() == "aarch64": + server = "Server = {}/$arch/$repo".format(args.mirror) + else: + server = "Server = {}/$repo/os/$arch".format(args.mirror) + + with open(os.path.join(workspace, "pacman.conf"), "w") as f: + f.write("""\ +[options] +HookDir = /no_hook/ +HoldPkg = pacman glibc +Architecture = auto +CheckSpace +SigLevel = Required DatabaseOptional + +[core] +{server} + +[extra] +{server} + +[community] +{server} +""".format(args=args, server=server)) + + subprocess.run(["pacman", "--color", "never", "--config", os.path.join(workspace, "pacman.conf"), "-Sy"], check=True) + c = subprocess.run(["pacman", "--color", "never", "--config", os.path.join(workspace, "pacman.conf"), "-Sg", "base"], stdout=subprocess.PIPE, universal_newlines=True, check=True) + packages = set(c.stdout.split()) + packages.remove("base") + + packages -= {"cryptsetup", + "device-mapper", + "dhcpcd", + "e2fsprogs", + "jfsutils", + "lvm2", + "mdadm", + "netctl", + "pcmciautils", + "reiserfsprogs", + "xfsprogs"} + + if args.bootable: + if args.output_format == OutputFormat.raw_gpt: + packages.add("e2fsprogs") + elif args.output_format == OutputFormat.raw_btrfs: + packages.add("btrfs-progs") + else: + if "linux" in packages: + packages.remove("linux") + + if args.packages is not None: + packages |= set(args.packages) + + if run_build_script and args.build_packages is not None: + packages |= set(args.build_packages) + + cmdline = ["pacstrap", + "-C", os.path.join(workspace, "pacman.conf"), + "-c", + "-d", + workspace + "/root"] + \ + list(packages) + + subprocess.run(cmdline, check=True) + + enable_networkd(workspace) + + print_step("Installing ArchLinux complete.") + +def install_distribution(args, workspace, run_build_script): + install = { + Distribution.fedora : install_fedora, + Distribution.debian : install_debian, + Distribution.ubuntu : install_ubuntu, + Distribution.arch : install_arch, + } + + install[args.distribution](args, workspace, run_build_script) + +def set_root_password(args, workspace): + "Set the root account password, or just delete it so it's easy to log in" + if args.password == '': + print_step("Deleting root password...") + jj = lambda line: (':'.join(['root', ''] + line.split(':')[2:]) + if line.startswith('root:') else line) + patch_file(os.path.join(workspace, 'root', 'etc/passwd'), jj) + elif args.password: + print_step("Setting root password...") + password = crypt.crypt(args.password, crypt.mksalt(crypt.METHOD_SHA512)) + jj = lambda line: (':'.join(['root', password] + line.split(':')[2:]) + if line.startswith('root:') else line) + patch_file(os.path.join(workspace, 'root', 'etc/shadow'), jj) + +def install_boot_loader_arch(args, workspace): + patch_file(os.path.join(workspace, "root", "etc/mkinitcpio.conf"), + lambda line: "HOOKS=\"systemd modconf block filesystems fsck\"\n" if line.startswith("HOOKS=") else line) + + kernel_version = next(filter(lambda x: x[0].isdigit(), os.listdir(os.path.join(workspace, "root", "lib/modules")))) + + run_workspace_command(workspace, + "/usr/bin/kernel-install", "add", kernel_version, "/boot/vmlinuz-linux") + +def install_boot_loader_debian(args, workspace): + kernel_version = next(filter(lambda x: x[0].isdigit(), os.listdir(os.path.join(workspace, "root", "lib/modules")))) + + run_workspace_command(workspace, + "/usr/bin/kernel-install", "add", kernel_version, "/boot/vmlinuz-" + kernel_version) + +def install_boot_loader(args, workspace): + if not args.bootable: + return + + print_step("Installing boot loader...") + + shutil.copyfile(os.path.join(workspace, "root", "usr/lib/systemd/boot/efi/systemd-bootx64.efi"), + os.path.join(workspace, "root", "boot/efi/EFI/systemd/systemd-bootx64.efi")) + + shutil.copyfile(os.path.join(workspace, "root", "usr/lib/systemd/boot/efi/systemd-bootx64.efi"), + os.path.join(workspace, "root", "boot/efi/EFI/BOOT/bootx64.efi")) + + if args.distribution == Distribution.arch: + install_boot_loader_arch(args, workspace) + + if args.distribution == Distribution.debian: + install_boot_loader_debian(args, workspace) + + print_step("Installing boot loader completed.") + +def enumerate_and_copy(source, dest, suffix = ""): + for entry in os.scandir(source + suffix): + dest_path = dest + suffix + "/" + entry.name + + if entry.is_dir(): + os.makedirs(dest_path, + mode=entry.stat(follow_symlinks=False).st_mode & 0o7777, + exist_ok=True) + enumerate_and_copy(source, dest, suffix + "/" + entry.name) + else: + try: + os.unlink(dest_path) + except: + pass + + shutil.copy(entry.path, dest_path, follow_symlinks=False) + + shutil.copystat(entry.path, dest_path, follow_symlinks=False) + +def install_extra_trees(args, workspace): + if args.extra_trees is None: + return + + print_step("Copying in extra file trees...") + + for d in args.extra_trees: + enumerate_and_copy(d, os.path.join(workspace, "root")) + + print_step("Copying in extra file trees completed.") + +def git_files_ignore(): + "Creates a function to be used as a ignore callable argument for copytree" + c = subprocess.run(['git', 'ls-files', '-z', '--others', '--cached', + '--exclude-standard', '--exclude', '/.mkosi-*'], + stdout=subprocess.PIPE, + universal_newlines=False, + check=True) + files = {x.decode("utf-8") for x in c.stdout.split(b'\0')} + del c + + def ignore(src, names): + return [name for name in names + if (os.path.relpath(os.path.join(src, name)) not in files + and not os.path.isdir(os.path.join(src, name)))] + return ignore + +def install_build_src(args, workspace, run_build_script): + if not run_build_script: + return + + if args.build_script is None: + return + + print_step("Copying in build script and sources...") + + shutil.copy(args.build_script, os.path.join(workspace, "root", "root", os.path.basename(args.build_script))) + + if args.build_sources is not None: + target = os.path.join(workspace, "root", "root/src") + use_git = args.use_git_files + if use_git is None: + use_git = os.path.exists('.git') + + if use_git: + ignore = git_files_ignore() + else: + ignore = shutil.ignore_patterns('.mkosi-*', '.git') + shutil.copytree(args.build_sources, target, symlinks=True, ignore=ignore) + + print_step("Copying in build script and sources completed.") + +def install_build_dest(args, workspace, run_build_script): + if run_build_script: + return + + if args.build_script is None: + return + + print_step("Copying in build tree...") + + enumerate_and_copy(os.path.join(workspace, "dest"), os.path.join(workspace, "root")) + + print_step("Copying in build tree completed.") + +def make_read_only(args, workspace): + if not args.read_only: + return + + if not args.output_format in (OutputFormat.raw_btrfs, OutputFormat.subvolume): + return + + print_step("Marking root subvolume read-only...") + + btrfs_subvol_make_ro(os.path.join(workspace, "root")) + + print_step("Marking root subvolume read-only completed.") + +def make_tar(args, workspace): + if args.output_format != OutputFormat.tar: + return None + + print_step("Creating archive...") + + f = tempfile.NamedTemporaryFile(dir = os.path.dirname(args.output), prefix=".mkosi-") + subprocess.run(["tar", "-C", os.path.join(workspace, "root"), "-c", "-J", "--xattrs", "--xattrs-include=*", "."], stdout=f, check=True) + + print_step("Creating archive completed.") + + return f + +def xz_output(args, raw): + if not args.output_format in (OutputFormat.raw_btrfs, OutputFormat.raw_gpt): + return raw + + if not args.xz: + return raw + + print_step("Compressing image file...") + + f = tempfile.NamedTemporaryFile(dir = os.path.dirname(args.output), prefix=".mkosi-") + subprocess.run(["xz", "-c", raw.name], stdout=f, check=True) + + print_step("Compressing image file complete.") + + return f + +def copy_nspawn_settings(args): + if args.nspawn_settings is None: + return None + + print_step("Copying nspawn settings file...") + + f = tempfile.NamedTemporaryFile(mode = "w+b", dir = os.path.dirname(args.output_nspawn_settings), prefix=".mkosi-") + + with open(args.nspawn_settings, "rb") as c: + bs = 65536 + buf = c.read(bs) + while len(buf) > 0: + f.write(buf) + buf = c.read(bs) + + print_step("Copying nspawn settings file completed.") + return f + +def hash_file(of, sf, fname): + bs = 65536 + h = hashlib.sha256() + + sf.seek(0) + buf = sf.read(bs) + while len(buf) > 0: + h.update(buf) + buf = sf.read(bs) + + of.write(h.hexdigest() + " *" + fname + "\n") + +def calculate_sha256sum(args, raw, tar, nspawn_settings): + if args.output_format in (OutputFormat.directory, OutputFormat.subvolume): + return None + + if not args.checksum: + return None + + print_step("Calculating SHA256SUM...") + + f = tempfile.NamedTemporaryFile(mode="w+", dir=os.path.dirname(args.output_checksum), prefix=".mkosi-", encoding="utf-8") + + if raw is not None: + hash_file(f, raw, os.path.basename(args.output)) + if tar is not None: + hash_file(f, tar, os.path.basename(args.output)) + if nspawn_settings is not None: + hash_file(f, nspawn_settings, os.path.basename(args.output_nspawn_settings)) + + print_step("Calculating SHA256SUM complete.") + return f + +def calculate_signature(args, checksum): + if not args.sign: + return None + + if checksum is None: + return None + + print_step("Signing SHA256SUM...") + + f = tempfile.NamedTemporaryFile(mode="wb", prefix=".mkosi-", dir=os.path.dirname(args.output_signature)) + + cmdline = ["gpg", "--detach-sign"] + + if args.key is not None: + cmdline.extend(["--default-key", args.key]) + + checksum.seek(0) + subprocess.run(cmdline, stdin=checksum, stdout=f, check=True) + + print_step("Signing SHA256SUM complete.") + + return f + +def link_output(args, workspace, raw, tar): + print_step("Linking image file...") + + if args.output_format in (OutputFormat.directory, OutputFormat.subvolume): + os.rename(os.path.join(workspace, "root"), args.output) + elif args.output_format in (OutputFormat.raw_btrfs, OutputFormat.raw_gpt): + os.chmod(raw, 0o666 & ~args.original_umask) + os.link(raw, args.output) + else: + os.chmod(raw, 0o666 & ~args.original_umask) + os.link(tar, args.output) + + print_step("Successfully linked " + args.output + ".") + +def link_output_nspawn_settings(args, path): + if path is None: + return + + print_step("Linking nspawn settings file...") + + os.chmod(path, 0o666 & ~args.original_umask) + os.link(path, args.output_nspawn_settings) + + print_step("Successfully linked " + args.output_nspawn_settings + ".") + +def link_output_checksum(args, checksum): + if checksum is None: + return + + print_step("Linking SHA256SUM file...") + + os.chmod(checksum, 0o666 & ~args.original_umask) + os.link(checksum, args.output_checksum) + + print_step("Successfully linked " + args.output_checksum + ".") + +def link_output_signature(args, signature): + if signature is None: + return + + print_step("Linking SHA256SUM.gpg file...") + + os.chmod(signature, 0o666 & ~args.original_umask) + os.link(signature, args.output_signature) + + print_step("Successfully linked " + args.output_signature + ".") + +def format_bytes(bytes): + if bytes >= 1024*1024*1024: + return "{:0.1f}G".format(bytes / 1024**3) + if bytes >= 1024*1024: + return "{:0.1f}M".format(bytes / 1024**2) + if bytes >= 1024: + return "{:0.1f}K".format(bytes / 1024) + + return "{}B".format(bytes) + +def dir_size(path): + sum = 0 + for entry in os.scandir(path): + if entry.is_symlink(): + # We can ignore symlinks because they either point into our tree, + # in which case we'll include the size of target directory anyway, + # or outside, in which case we don't need to. + continue + elif entry.is_file(): + sum += entry.stat().st_blocks * 512 + elif entry.is_dir(): + sum += dir_size(entry.path) + return sum + +def print_output_size(args): + if args.output_format in (OutputFormat.directory, OutputFormat.subvolume): + print_step("Resulting image size is " + format_bytes(dir_size(args.output)) + ".") + else: + st = os.stat(args.output) + print_step("Resulting image size is " + format_bytes(st.st_size) + ", consumes " + format_bytes(st.st_blocks * 512) + ".") + +def setup_cache(args): + if not args.distribution in (Distribution.fedora, Distribution.debian, Distribution.ubuntu): + return None + + print_step("Setting up package cache...") + + if args.cache_path is None: + d = tempfile.TemporaryDirectory(dir=os.path.dirname(args.output), prefix=".mkosi-") + args.cache_path = d.name + else: + os.makedirs(args.cache_path, 0o700, True) + d = None + + print_step("Setting up package cache " + args.cache_path + " completed.") + return d + +class PackageAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + l = getattr(namespace, self.dest) + if l is None: + l = [] + l.extend(values.split(",")) + setattr(namespace, self.dest, l) + +def parse_args(): + parser = argparse.ArgumentParser(description='Build Legacy-Free OS Images', add_help=False) + + group = parser.add_argument_group("Commands") + group.add_argument("verb", choices=("build", "clean", "help", "summary"), nargs='?', default="build", help='Operation to execute') + group.add_argument('-h', '--help', action='help', help="Show this help") + + group = parser.add_argument_group("Distribution") + group.add_argument('-d', "--distribution", choices=Distribution.__members__, help='Distribution to install') + group.add_argument('-r', "--release", help='Distribution release to install') + group.add_argument('-m', "--mirror", help='Distribution mirror to use') + + group = parser.add_argument_group("Output") + group.add_argument('-t', "--format", dest='output_format', choices=OutputFormat.__members__, help='Output Format') + group.add_argument('-o', "--output", help='Output image path', metavar='PATH') + group.add_argument('-f', "--force", action='store_true', help='Remove existing image file before operation') + group.add_argument('-b', "--bootable", type=parse_boolean, nargs='?', const=True, + help='Make image bootable on EFI (only raw_gpt, raw_btrfs)') + group.add_argument("--read-only", action='store_true', help='Make root volume read-only (only raw_btrfs, subvolume)') + group.add_argument("--compress", action='store_true', help='Enable compression in file system (only raw_btrfs, subvolume)') + group.add_argument("--xz", action='store_true', help='Compress resulting image with xz (only raw_gpt, raw_btrfs, implied on tar)') + + group = parser.add_argument_group("Packages") + group.add_argument('-p', "--package", action=PackageAction, dest='packages', help='Add an additional package to the OS image', metavar='PACKAGE') + group.add_argument("--with-docs", action='store_true', help='Install documentation (only fedora)') + group.add_argument("--cache", dest='cache_path', help='Package cache path (only fedora, debian, ubuntu)', metavar='PATH') + group.add_argument("--extra-tree", action='append', dest='extra_trees', help='Copy an extra tree on top of image', metavar='PATH') + group.add_argument("--build-script", help='Build script to run inside image', metavar='PATH') + group.add_argument("--build-sources", help='Path for sources to build', metavar='PATH') + group.add_argument("--build-package", action=PackageAction, dest='build_packages', help='Additional packages needed for build script', metavar='PACKAGE') + group.add_argument('--use-git-files', type=parse_boolean, + help='Ignore any files that git itself ignores (default: guess)') + group.add_argument("--settings", dest='nspawn_settings', help='Add in .spawn settings file', metavar='PATH') + + group = parser.add_argument_group("Partitions") + group.add_argument("--root-size", help='Set size of root partition (only raw_gpt, raw_btrfs)', metavar='BYTES') + group.add_argument("--esp-size", help='Set size of EFI system partition (only raw_gpt, raw_btrfs)', metavar='BYTES') + group.add_argument("--swap-size", help='Set size of swap partition (only raw_gpt, raw_btrfs)', metavar='BYTES') + group.add_argument("--home-size", help='Set size of /home partition (only raw_gpt)', metavar='BYTES') + group.add_argument("--srv-size", help='Set size of /srv partition (only raw_gpt)', metavar='BYTES') + + group = parser.add_argument_group("Validation (only raw_gpt, raw_btrfs, tar)") + group.add_argument("--checksum", action='store_true', help='Write SHA256SUM file') + group.add_argument("--sign", action='store_true', help='Write and sign SHA256SUM file') + group.add_argument("--key", help='GPG key to use for signing') + group.add_argument("--password", help='Set the root password') + + group = parser.add_argument_group("Additional Configuration") + group.add_argument('-C', "--directory", help='Change to specified directory before doing anything', metavar='PATH') + group.add_argument("--default", dest='default_path', help='Read configuration data from file', metavar='PATH') + group.add_argument("--kernel-commandline", help='Set the kernel command line (only bootable images)') + + args = parser.parse_args() + + if args.verb == "help": + parser.print_help() + sys.exit(0) + + return args + +def parse_bytes(bytes): + if bytes is None: + return bytes + + if bytes.endswith('G'): + factor = 1024**3 + elif bytes.endswith('M'): + factor = 1024**2 + elif bytes.endswith('K'): + factor = 1024 + else: + factor = 1 + + if factor > 1: + bytes = bytes[:-1] + + result = int(bytes) * factor + if result <= 0: + raise ValueError("Size out of range") + + if result % 512 != 0: + raise ValueError("Size not a multiple of 512") + + return result + +def detect_distribution(): + try: + f = open("/etc/os-release") + except IOError: + try: + f = open("/usr/lib/os-release") + except IOError: + return None, None + + id = None + version_id = None + + for ln in f: + if ln.startswith("ID="): + id = ln[3:].strip() + if ln.startswith("VERSION_ID="): + version_id = ln[11:].strip() + + d = Distribution.__members__.get(id, None) + return d, version_id + +def unlink_try_hard(path): + try: + os.unlink(path) + except: + pass + + try: + btrfs_subvol_delete(path) + except: + pass + + try: + shutil.rmtree(path) + except: + pass + +def unlink_output(args): + if not args.force and args.verb != "clean": + return + + unlink_try_hard(args.output) + + if args.checksum: + unlink_try_hard(args.output_checksum) + + if args.sign: + unlink_try_hard(args.output_signature) + + if args.nspawn_settings is not None: + unlink_try_hard(args.output_nspawn_settings) + +def parse_boolean(s): + if s in {"1", "true", "yes"}: + return True + + if s in {"0", "false", "no"}: + return False + + raise ValueError("invalid literal for bool(): {!r}".format(s)) + +def process_setting(args, section, key, value): + if section == "Distribution": + if key == "Distribution": + if args.distribution is None: + args.distribution = value + elif key == "Release": + if args.release is None: + args.release = value + elif key is None: + return True + else: + return False + elif section == "Output": + if key == "Format": + if args.output_format is None: + args.output_format = value + elif key == "Output": + if args.output is None: + args.output = value + elif key == "Force": + if not args.force: + args.force = parse_boolean(value) + elif key == "Bootable": + if not args.bootable: + args.bootable = parse_boolean(value) + elif key == "ReadOnly": + if not args.read_only: + args.read_only = parse_boolean(value) + elif key == "Compress": + if not args.compress: + args.compress = parse_boolean(value) + elif key == "XZ": + if not args.xz: + args.xz = parse_boolean(value) + elif key is None: + return True + else: + return False + elif section == "Packages": + if key == "Packages": + if args.packages is None: + args.packages = value.split() + else: + args.packages.extend(value.split()) + elif key == "WithDocs": + if not args.with_docs: + args.with_docs = parse_boolean(value) + elif key == "Cache": + if args.cache_path is None: + args.cache_path = value + elif key == "ExtraTrees": + if args.extra_trees is None: + args.extra_trees = value.split() + else: + args.extra_trees.extend(value.split()) + elif key == "BuildScript": + if args.build_script is not None: + args.build_script = value + elif key == "BuildSources": + if args.build_sources is not None: + args.build_sources = value + elif key == "BuildPackages": + if args.build_packages is None: + args.build_packages = value.split() + else: + args.build_packages.extend(value.split()) + elif key == "NSpawnSettings": + if args.nspawn_settings is not None: + args.nspawn_settings = value + elif key is None: + return True + else: + return False + elif section == "Partitions": + if key == "RootSize": + if args.root_size is None: + args.root_size = value + elif key == "ESPSize": + if args.esp_size is None: + args.esp_size = value + elif key == "SwapSize": + if args.swap_size is None: + args.swap_size = value + elif key == "HomeSize": + if args.home_size is None: + args.home_size = value + elif key == "SrvSize": + if args.srv_size is None: + args.srv_size = value + elif key is None: + return True + else: + return False + elif section == "Validation": + if key == "CheckSum": + if not args.checksum: + args.checksum = parse_boolean(value) + elif key == "Sign": + if not args.sign: + args.sign = parse_boolean(value) + elif key == "Key": + if args.key is None: + args.key = value + elif key == "Password": + if args.password is None: + args.password = value + elif key is None: + return True + else: + return False + else: + return False + + return True + +def load_defaults(args): + fname = "mkosi.default" if args.default_path is None else args.default_path + + try: + f = open(fname, "r") + except FileNotFoundError: + return + + config = configparser.ConfigParser(delimiters='=') + config.optionxform = str + config.read_file(f) + + for section in config.sections(): + if not process_setting(args, section, None, None): + sys.stderr.write("Unknown section in {}, ignoring: [{}]\n".format(fname, section)) + + for key in config[section]: + if not process_setting(args, section, key, config[section][key]): + sys.stderr.write("Unknown key in section [{}] in {}, ignoring: {}=\n".format(section, fname, key)) + +def find_nspawn_settings(args): + if args.nspawn_settings is not None: + return + + if os.path.exists("mkosi.nspawn"): + args.nspawn_settings = "mkosi.nspawn" + +def find_extra(args): + if os.path.exists("mkosi.extra"): + if args.extra_trees is None: + args.extra_trees = ["mkosi.extra"] + else: + args.extra_trees.append("mkosi.extra") + +def find_build_script(args): + if args.build_script is not None: + return + + if os.path.exists("mkosi.build"): + args.build_script = "mkosi.build" + +def find_build_sources(args): + if args.build_sources is not None: + return + + args.build_sources = os.getcwd() + +def build_nspawn_settings_path(path): + t = path + while True: + if t.endswith(".xz"): + t = t[:-3] + elif t.endswith(".raw"): + t = t[:-4] + elif t.endswith(".tar"): + t = t[:-4] + else: + break + + return t + ".nspawn" + +def load_args(): + args = parse_args() + + if args.directory is not None: + os.chdir(args.directory) + + load_defaults(args) + find_nspawn_settings(args) + find_extra(args) + find_build_script(args) + find_build_sources(args) + + if args.output_format is None: + args.output_format = OutputFormat.raw_gpt + else: + args.output_format = OutputFormat[args.output_format] + + if args.distribution is not None: + args.distribution = Distribution[args.distribution] + + if args.distribution is None or args.release is None: + d, r = detect_distribution() + + if args.distribution is None: + args.distribution = d + + if args.distribution == d and args.release is None: + args.release = r + + if args.distribution is None: + sys.stderr.write("Couldn't detect distribution.\n") + sys.exit(1) + + if args.release is None: + if args.distribution == Distribution.fedora: + args.release = "24" + elif args.distribution == Distribution.debian: + args.release = "unstable" + elif args.distribution == Distribution.ubuntu: + args.release = "yakkety" + + if args.mirror is None: + if args.distribution == Distribution.fedora: + args.mirror = None + elif args.distribution == Distribution.debian: + args.mirror = "http://httpredir.debian.org/debian" + elif args.distribution == Distribution.ubuntu: + args.mirror = "http://archive.ubuntu.com/ubuntu" + elif args.distribution == Distribution.arch: + args.mirror = "https://mirrors.kernel.org/archlinux" + if platform.machine() == "aarch64": + args.mirror = "http://mirror.archlinuxarm.org" + + if args.bootable: + if args.distribution not in (Distribution.fedora, Distribution.arch, Distribution.debian): + sys.stderr.write("Bootable images are currently supported only on Debian, Fedora and ArchLinux.\n") + sys.exit(1) + + if not args.output_format in (OutputFormat.raw_gpt, OutputFormat.raw_btrfs): + sys.stderr.write("Directory, subvolume and tar images cannot be booted.\n") + sys.exit(1) + + if args.sign: + args.checksum = True + + if args.output is None: + if args.output_format in (OutputFormat.raw_gpt, OutputFormat.raw_btrfs): + if args.xz: + args.output = "image.raw.xz" + else: + args.output = "image.raw" + elif args.output_format == OutputFormat.tar: + args.output = "image.tar.xz" + else: + args.output = "image" + + args.output = os.path.abspath(args.output) + + if args.output_format == OutputFormat.tar: + args.xz = True + + if args.checksum: + args.output_checksum = os.path.join(os.path.dirname(args.output), "SHA256SUM") + + if args.sign: + args.output_signature = os.path.join(os.path.dirname(args.output), "SHA256SUM.gpg") + + if args.nspawn_settings is not None: + args.nspawn_settings = os.path.abspath(args.nspawn_settings) + args.output_nspawn_settings = build_nspawn_settings_path(args.output) + + if args.build_script is not None: + args.build_script = os.path.abspath(args.build_script) + + if args.build_sources is not None: + args.build_sources = os.path.abspath(args.build_sources) + + if args.extra_trees is not None: + for i in range(len(args.extra_trees)): + args.extra_trees[i] = os.path.abspath(args.extra_trees[i]) + + args.root_size = parse_bytes(args.root_size) + args.home_size = parse_bytes(args.home_size) + args.srv_size = parse_bytes(args.srv_size) + args.esp_size = parse_bytes(args.esp_size) + args.swap_size = parse_bytes(args.swap_size) + + if args.output_format in (OutputFormat.raw_gpt, OutputFormat.raw_btrfs) and args.root_size is None: + args.root_size = 1024*1024*1024 + + if args.bootable and args.esp_size is None: + args.esp_size = 256*1024*1024 + + if args.bootable and args.kernel_commandline is None: + args.kernel_commandline = "rhgb quiet selinux=0 audit=0 rw" + + return args + +def check_output(args): + for f in (args.output, + args.output_checksum if args.checksum else None, + args.output_signature if args.sign else None, + args.output_nspawn_settings if args.nspawn_settings is not None else None): + + if f is None: + continue + + if os.path.exists(f): + sys.stderr.write("Output file " + f + " exists already. (Consider invocation with --force.)\n") + sys.exit(1) + +def yes_no(b): + return "yes" if b else "no" + +def format_bytes_or_disabled(sz): + if sz is None: + return "(disabled)" + + return format_bytes(sz) + +def none_to_na(s): + return "n/a" if s is None else s + +def none_to_none(s): + return "none" if s is None else s + +def line_join_list(l): + + if l is None: + return "none" + + return "\n ".join(l) + +def print_summary(args): + sys.stderr.write("DISTRIBUTION:\n") + sys.stderr.write(" Distribution: " + args.distribution.name + "\n") + sys.stderr.write(" Release: " + none_to_na(args.release) + "\n") + if args.mirror is not None: + sys.stderr.write(" Mirror: " + args.mirror + "\n") + sys.stderr.write("\nOUTPUT:\n") + sys.stderr.write(" Output Format: " + args.output_format.name + "\n") + sys.stderr.write(" Output: " + args.output + "\n") + sys.stderr.write(" Output Checksum: " + none_to_na(args.output_checksum if args.checksum else None) + "\n") + sys.stderr.write(" Output Signature: " + none_to_na(args.output_signature if args.sign else None) + "\n") + sys.stderr.write("Output nspawn Settings: " + none_to_na(args.output_nspawn_settings if args.nspawn_settings is not None else None) + "\n") + + if args.output_format in (OutputFormat.raw_btrfs, OutputFormat.subvolume): + sys.stderr.write(" Read-only: " + yes_no(args.read_only) + "\n") + sys.stderr.write(" FS Compression: " + yes_no(args.compress) + "\n") + + if args.output_format in (OutputFormat.raw_gpt, OutputFormat.raw_btrfs, OutputFormat.tar): + sys.stderr.write(" XZ Compression: " + yes_no(args.xz) + "\n") + + sys.stderr.write("\nPACKAGES:\n") + sys.stderr.write(" Packages: " + line_join_list(args.packages) + "\n") + + if args.distribution == Distribution.fedora: + sys.stderr.write(" With Documentation: " + yes_no(args.with_docs) + "\n") + if args.distribution in (Distribution.fedora, Distribution.debian, Distribution.ubuntu): + sys.stderr.write(" Package Cache: " + none_to_none(args.cache_path) + "\n") + + sys.stderr.write(" Extra Trees: " + line_join_list(args.extra_trees) + "\n") + sys.stderr.write(" Build Script: " + none_to_none(args.build_script) + "\n") + sys.stderr.write(" Build Sources: " + none_to_none(args.build_sources) + "\n") + sys.stderr.write(" Build Packages: " + line_join_list(args.build_packages) + "\n") + sys.stderr.write(" nspawn Settings: " + none_to_none(args.nspawn_settings) + "\n") + + if args.output_format in (OutputFormat.raw_gpt, OutputFormat.raw_btrfs): + sys.stderr.write("\nPARTITIONS:\n") + sys.stderr.write(" Bootable: " + yes_no(args.bootable) + "\n") + sys.stderr.write(" Root Partition: " + format_bytes(args.root_size) + "\n") + sys.stderr.write(" Swap Partition: " + format_bytes_or_disabled(args.swap_size) + "\n") + sys.stderr.write(" ESP: " + format_bytes_or_disabled(args.esp_size) + "\n") + sys.stderr.write(" /home Partition: " + format_bytes_or_disabled(args.home_size) + "\n") + sys.stderr.write(" /srv Partition: " + format_bytes_or_disabled(args.srv_size) + "\n") + + if args.output_format in (OutputFormat.raw_gpt, OutputFormat.raw_btrfs, OutputFormat.tar): + sys.stderr.write("\nVALIDATION:\n") + sys.stderr.write(" Checksum: " + yes_no(args.checksum) + "\n") + sys.stderr.write(" Sign: " + yes_no(args.sign) + "\n") + sys.stderr.write(" GPG Key: " + ("default" if args.key is None else args.key) + "\n") + sys.stderr.write(" Password: " + ("default" if args.password is None else args.password) + "\n") + +def build_image(args, workspace, run_build_script): + # If there's no build script set, there's no point in executing + # the build script iteration. Let's quite early. + if args.build_script is None and run_build_script: + return (None, None) + + tar = None + + raw = create_image(args, workspace.name) + with attach_image_loopback(args, raw) as loopdev: + prepare_swap(args, loopdev) + prepare_esp(args, loopdev) + prepare_root(args, loopdev) + prepare_home(args, loopdev) + prepare_srv(args, loopdev) + + with mount_image(args, workspace.name, loopdev): + prepare_tree(args, workspace.name) + mount_cache(args, workspace.name) + install_distribution(args, workspace.name, run_build_script) + install_boot_loader(args, workspace.name) + install_extra_trees(args, workspace.name) + install_build_src(args, workspace.name, run_build_script) + install_build_dest(args, workspace.name, run_build_script) + + if not run_build_script: + set_root_password(args, workspace.name) + make_read_only(args, workspace.name) + tar = make_tar(args, workspace.name) + + return raw, tar + +def run_build_script(args, workspace, raw): + if args.build_script is None: + return + + print_step("Running build script...") + + dest = os.path.join(workspace, "dest") + os.mkdir(dest, 0o755) + + cmdline = ["systemd-nspawn", + '--quiet', + "--directory=" + os.path.join(workspace, "root") if raw is None else "--image=" + raw.name, + "--as-pid2", + "--private-network", + "--register=no", + "--bind", dest + ":/root/dest", + "--setenv=WITH_DOCS=" + ("1" if args.with_docs else "0"), + "--setenv=DESTDIR=/root/dest"] + + if args.build_sources is not None: + cmdline.append("--setenv=SRCDIR=/root/src") + cmdline.append("--chdir=/root/src") + else: + cmdline.append("--chdir=/root") + + cmdline.append("/root/" + os.path.basename(args.build_script)) + + print(cmdline) + subprocess.run(cmdline, check=True) + + print_step("Running build script completed.") + +def build_stuff(args): + cache = setup_cache(args) + workspace = setup_workspace(args) + + # Run the image builder twice, once for running the build script and once for the final build + raw, tar = build_image(args, workspace, run_build_script=True) + + run_build_script(args, workspace.name, raw) + + if raw is not None: + del raw + + if tar is not None: + del tar + + raw, tar = build_image(args, workspace, run_build_script=False) + + raw = xz_output(args, raw) + settings = copy_nspawn_settings(args) + checksum = calculate_sha256sum(args, raw, tar, settings) + signature = calculate_signature(args, checksum) + + link_output(args, + workspace.name, + raw.name if raw is not None else None, + tar.name if tar is not None else None) + + link_output_checksum(args, + checksum.name if checksum is not None else None) + + link_output_signature(args, + signature.name if signature is not None else None) + + link_output_nspawn_settings(args, + settings.name if settings is not None else None) + + +def main(): + args = load_args() + + if os.getuid() != 0: + sys.stderr.write("Must be invoked as root.\n") + sys.exit(1) + + if args.verb in ("build", "clean"): + unlink_output(args) + + if args.verb == "build": + check_output(args) + + if args.verb in ("build", "summary"): + print_summary(args) + + if args.verb == "build": + init_namespace(args) + build_stuff(args) + print_output_size(args) + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..8e41779 --- /dev/null +++ b/setup.py @@ -0,0 +1,14 @@ +#!/usr/bin/python3 + +from setuptools import setup + +setup( + name="mkosi", + version="1", + description="Create legacy-free OS images", + url="https://github.com/systemd/mkosi", + maintainer="mkosi contributors", + maintainer_email="systemd-devel@lists.freedesktop.org", + license="LGPLv2+", + scripts=["mkosi"], +) |