├── .gitignore ├── COPYRIGHT ├── LICENSE ├── MANIFEST.in ├── README.rst ├── completions ├── _depthchargectl.bash ├── _depthchargectl.zsh ├── _mkdepthcharge.bash └── _mkdepthcharge.zsh ├── depthcharge_tools ├── __init__.py ├── boards.ini ├── config.ini ├── depthchargectl │ ├── __init__.py │ ├── __main__.py │ ├── _bless.py │ ├── _build.py │ ├── _check.py │ ├── _config.py │ ├── _list.py │ ├── _remove.py │ ├── _target.py │ └── _write.py ├── mkdepthcharge.py └── utils │ ├── __init__.py │ ├── argparse.py │ ├── collections.py │ ├── os.py │ ├── pathlib.py │ ├── platform.py │ ├── string.py │ └── subprocess.py ├── depthchargectl.rst ├── init.d └── depthchargectl-bless ├── mkdepthcharge.rst ├── setup.py ├── systemd ├── 90-depthcharge-tools.install └── depthchargectl-bless.service └── update_config.py /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | *.pyc 3 | __pycache__ 4 | *.egg-info 5 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: depthcharge-tools 3 | Upstream-Contact: Alper Nebi Yasak 4 | Source: https://github.com/alpernebbi/depthcharge-tools/ 5 | License: GPL-2+ 6 | 7 | Files: * 8 | Copyright: 2019-2023 Alper Nebi Yasak 9 | License: GPL-2+ 10 | 11 | Files: systemd/depthchargectl-bless.service 12 | Comment: 13 | This file is modified from systemd's systemd-bless-boot.service. 14 | Author attributions are derived from systemd git log. 15 | Copyright: 2019 Alper Nebi Yasak 16 | 2018 Lennart Poettering 17 | License: LGPL-2.1+ 18 | systemd is free software; you can redistribute it and/or modify it 19 | under the terms of the GNU Lesser General Public License as published by 20 | the Free Software Foundation; either version 2.1 of the License, or 21 | (at your option) any later version. 22 | . 23 | On Debian systems, the complete text of the GNU Lesser General Public 24 | License version 2.1 can be found in ‘/usr/share/common-licenses/LGPL-2.1’. 25 | 26 | Files: systemd/90-depthcharge-tools.install 27 | Comment: 28 | This file is modified from systemd's 90-loaderentry.install. 29 | Author attributions are derived from systemd git log. 30 | Copyright: 2022 Alper Nebi Yasak 31 | 2022 Michael Biebl 32 | 2022 Antonio Alvarez Feijoo 33 | 2021-2022 наб 34 | 2020 Kir Kolyshkin 35 | 2020 Jörg Thalheim 36 | 2019 Marc-Antoine Perennou 37 | 2019-2022 Zbigniew Jędrzejewski-Szmek 38 | 2018 Javier Martinez Canillas 39 | 2018-2019 Mike Auty 40 | 2017-2021 Yu Watanabe 41 | 2014 Michael Chapman 42 | 2014-2022 Lennart Poettering 43 | 2013 Tom Gundersen 44 | 2013-2016 Harald Hoyer 45 | License: LGPL-2.1+ 46 | systemd is free software; you can redistribute it and/or modify it 47 | under the terms of the GNU Lesser General Public License as published by 48 | the Free Software Foundation; either version 2.1 of the License, or 49 | (at your option) any later version. 50 | . 51 | systemd is distributed in the hope that it will be useful, but 52 | WITHOUT ANY WARRANTY; without even the implied warranty of 53 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 54 | General Public License for more details. 55 | . 56 | You should have received a copy of the GNU Lesser General Public License 57 | along with systemd; If not, see . 58 | . 59 | On Debian systems, the complete text of the GNU Lesser General Public 60 | License version 2.1 can be found in ‘/usr/share/common-licenses/LGPL-2.1’. 61 | 62 | License: GPL-2+ 63 | This program is free software; you can redistribute it and/or modify 64 | it under the terms of the GNU General Public License as published by 65 | the Free Software Foundation; either version 2 of the License, or 66 | (at your option) any later version. 67 | . 68 | This program is distributed in the hope that it will be useful, 69 | but WITHOUT ANY WARRANTY; without even the implied warranty of 70 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 71 | GNU General Public License for more details. 72 | . 73 | You should have received a copy of the GNU General Public License 74 | along with this program. If not, see 75 | . 76 | On Debian systems, the complete text of the GNU General Public License 77 | version 2 can be found in "/usr/share/common-licenses/GPL-2". 78 | 79 | # vi: set ft=debcopyright : 80 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | # depthcharge-tools python package manifest 4 | # Copyright (C) 2021 Alper Nebi Yasak 5 | # See COPYRIGHT and LICENSE files for full copyright information. 6 | 7 | include COPYRIGHT LICENSE *.rst 8 | recursive-include completions * 9 | recursive-include init.d * 10 | recursive-include systemd * 11 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | .. depthcharge-tools README file 4 | .. Copyright (C) 2019-2023 Alper Nebi Yasak 5 | .. See COPYRIGHT and LICENSE files for full copyright information. 6 | 7 | ================= 8 | Depthcharge-Tools 9 | ================= 10 | This project is a collection of tools that ease and automate interacting 11 | with depthcharge_, the Chrome OS bootloader. 12 | 13 | Depthcharge is built into the firmware of Chrome OS boards, uses a 14 | custom verified boot flow and usually cannot boot other operating 15 | systems as is. This means someone who wants to use e.g. Debian_ on these 16 | boards need to either replace the firmware or work their system into 17 | `the format depthcharge expects`_. These tools are about the latter. 18 | 19 | Right now these are developed on and tested with only a few boards, 20 | but everything will attempt to work on other boards based on my best 21 | guesses. 22 | 23 | .. _depthcharge: https://chromium.googlesource.com/chromiumos/platform/depthcharge 24 | .. _the format depthcharge expects: https://chromium.googlesource.com/chromiumos/docs/+/HEAD/disk_format.md#Google-ChromeOS-devices 25 | .. _Debian: https://www.debian.org/ 26 | 27 | 28 | mkdepthcharge 29 | ============= 30 | The mkdepthcharge_ tool is intended to wrap mkimage_ and vbutil_kernel_ 31 | to provide reasonable defaults to them, hide their idiosyncrasies and 32 | automate creating a depthcharge-bootable partition image appropriate for 33 | the running architecture. An example invocation on a Samsung Chromebook 34 | Plus (v1, arm64) could be:: 35 | 36 | $ mkdepthcharge -o depthcharge.img --compress lzma \ 37 | --cmdline "console=tty1 root=/dev/mmcblk0p2 rootwait" -- \ 38 | /boot/vmlinuz.gz /boot/initrd.img rk3399-gru-kevin.dtb 39 | 40 | Here, mkdepthcharge would automatically extract and recompress the 41 | kernel, create a FIT image, put command line parameters into a file, 42 | create an empty bootloader, and provide defaults for vboot keys and 43 | other arguments while building the partition image. 44 | 45 | .. _mkdepthcharge: https://github.com/alpernebbi/depthcharge-tools/blob/master/mkdepthcharge.rst 46 | .. _mkimage: https://dyn.manpages.debian.org/jump?q=unstable/mkimage 47 | .. _vbutil_kernel: https://dyn.manpages.debian.org/jump?q=unstable/vbutil_kernel 48 | 49 | 50 | depthchargectl 51 | ============== 52 | The depthchargectl_ tool goes a step further and aims to fully automate 53 | bootable image creation and Chrome OS kernel partition management, even 54 | the board-specific and distro-specific parts. With proper integration 55 | with your distribution, depthchargectl can keep your system bootable 56 | across kernel and initramfs changes without any interaction on your 57 | part. Even without such integration, a single command automates most of 58 | the work:: 59 | 60 | # Use --allow-current if you only have one Chrome OS kernel partition. 61 | $ sudo depthchargectl write --allow-current 62 | Building depthcharge image for board 'Samsung Chromebook Plus' ('kevin'). 63 | Built depthcharge image for kernel version '5.10.0-6-arm64'. 64 | Wrote image '/boot/depthcharge/5.10.0-6-arm64.img' to partition '/dev/mmcblk1p1'. 65 | Set partition '/dev/mmcblk1p1' as next to boot. 66 | 67 | # After a reboot, you or an init service should run this. 68 | $ sudo depthchargectl bless 69 | Set partition '/dev/mmcblk1p1' as successfully booted. 70 | 71 | .. _depthchargectl: https://github.com/alpernebbi/depthcharge-tools/blob/master/depthchargectl.rst 72 | 73 | 74 | Installation 75 | ============ 76 | This depends on the ``pkg_resources`` Python package which is usually 77 | distributed with ``setuptools``. The tools can run a number of programs 78 | when necessary, which should be considered dependencies: 79 | 80 | - ``futility`` (``vbutil_kernel``), ``cgpt``, ``crossystem`` 81 | - ``mkimage``, ``fdtget``, ``fdtput`` 82 | - ``lz4``, ``lzma`` 83 | - ``gzip``, ``lzop``, ``bzip2``, ``xz``, ``zstd`` 84 | (optional, for unpacking compressed ``/boot/vmlinuz``) 85 | 86 | The ``rst2man`` program (from ``docutils``) should be used to convert 87 | the ``mkdepthcharge.rst`` and ``depthchargectl.rst`` files to manual 88 | pages. However, this is not automated here and has to be done manually. 89 | 90 | This project (or at least ``depthchargectl``) is meant to be integrated 91 | into your operating system by its maintainers, and the best way to 92 | install it is through your OS' package manager whenever possible. 93 | 94 | 95 | Debian 96 | ------ 97 | An official `depthcharge-tools Debian package`_ is available upstream, 98 | since Debian 12 (bookworm). You can install it like any other package:: 99 | 100 | $ sudo apt install depthcharge-tools 101 | 102 | It includes the necessary system hooks and services to make and keep 103 | your Chromebook bootable, enabled by default. These however do not 104 | trigger on the depthcharge-tools installation, but on kernel and 105 | initramfs changes. To trigger these hooks manually, run:: 106 | 107 | $ sudo update-initramfs -u 108 | 109 | .. _depthcharge-tools Debian package: https://packages.debian.org/sid/depthcharge-tools 110 | 111 | 112 | Alpine Linux 113 | ------------ 114 | Thanks to the efforts in supporting `postmarketOS on ChromeOS Devices`_, 115 | there is an official `depthcharge-tools package for Alpine Linux`_. You 116 | should be able to install it as:: 117 | 118 | $ sudo apk add depthcharge-tools 119 | 120 | However, this doesn't include any system hooks or services to keep your 121 | Chromebook bootable. 122 | 123 | .. _postmarketOS on ChromeOS Devices: https://wiki.postmarketos.org/wiki/Chrome_OS_devices 124 | .. _depthcharge-tools package for Alpine Linux: https://pkgs.alpinelinux.org/package/edge/testing/x86/depthcharge-tools 125 | 126 | 127 | Pip 128 | --- 129 | Python binary wheels are uploaded to PyPI_, and it should be possible to 130 | install the python package using `pip`. However, this does not install 131 | the manual pages, bash/zsh completions, systemd/init.d service files, 132 | and OS-specific kernel/initramfs hooks. 133 | 134 | You can install in `--user` mode, but this makes it quite hard to use 135 | `depthchargectl` as root. As root privileges are necessary to manipulate 136 | system block devices this limits you a bit:: 137 | 138 | $ pip install --user depthcharge-tools 139 | 140 | Although inadvisable, you can install as root to overcome that caveat. 141 | Alternatively, see the `PYTHONPATH` hack in one of the later sections. 142 | 143 | .. _PyPI: https://pypi.org/project/depthcharge-tools/ 144 | 145 | 146 | Configuration 147 | ============= 148 | You can configure depthcharge-tools with the |CONFIG_FILE| file, or by 149 | putting similar fragments in the |CONFIGD_DIR| directory. See the 150 | config.ini_ file for the built-in default configuration. 151 | 152 | Settings in the ``[depthcharge-tools]`` section are the global defaults 153 | from which all commands inherit. Other than that, config sections have 154 | inheritence based on their names i.e. those in the form of ``[a/b/c]`` 155 | inherit from ``[a/b]`` which also inherits from ``[a]``. Each subcommand 156 | reads its config from such a subsection. 157 | 158 | Currently the following configuration options are available:: 159 | 160 | [depthcharge-tools] 161 | enable-system-hooks: Write/remove images on kernel/initramfs changes 162 | vboot-keyblock: The kernel keyblock file for verifying and signing images 163 | vboot-private-key: The private key (.vbprivk) for signing images 164 | vboot-public-key: The public key for (.vbpubk) verifying images 165 | 166 | [depthchargectl] 167 | board: Codename of a board to build and check images for 168 | ignore-initramfs: Do not include an initramfs in the image 169 | images-dir: Directory to store built images 170 | kernel-cmdline: Kernel commandline parameters to use 171 | zimage-initramfs-hack = How to support initramfs on x86 boards 172 | 173 | For longer explanations check the manual pages of each command for 174 | options named the same as these. 175 | 176 | .. |CONFIG_FILE| replace:: ``/etc/depthcharge-tools/config`` 177 | .. |CONFIGD_DIR| replace:: ``/etc/depthcharge-tools/config.d`` 178 | .. _config.ini: https://github.com/alpernebbi/depthcharge-tools/blob/master/depthcharge_tools/config.ini 179 | 180 | 181 | Installation for development 182 | ============================ 183 | If you want to use development versions, you can clone this repository 184 | and install using pip:: 185 | 186 | $ pip3 install --user -e /path/to/depthcharge-tools 187 | 188 | Hopefully, you should be able to use depthchargectl with just that:: 189 | 190 | $ depthchargectl build --output depthcharge.img 191 | Building depthcharge image for board 'Samsung Chromebook Plus' ('kevin'). 192 | Built depthcharge image for kernel version '5.10.0-6-arm64'. 193 | depthchargectl.img 194 | 195 | Most ``depthchargectl`` functionality needs root as it handles disks and 196 | partitions, and you need special care while invoking as root:: 197 | 198 | $ depthchargectl() { 199 | sudo PYTHONPATH=/path/to/depthcharge-tools \ 200 | python3 -m depthcharge_tools.depthchargectl "$@" 201 | } 202 | 203 | $ depthchargectl list /dev/mmcblk0 204 | S P T PATH 205 | 1 2 0 /dev/mmcblk0p2 206 | 1 1 0 /dev/mmcblk0p4 207 | 0 0 15 /dev/mmcblk0p6 208 | 209 | Or you can add a similar invocation to the /usr/local/bin files, so that 210 | it's available to both normal users and root:: 211 | 212 | $ sudo tee /usr/local/bin/depthchargectl < 225 | 226 | This program is free software; you can redistribute it and/or modify 227 | it under the terms of the GNU General Public License as published by 228 | the Free Software Foundation; either version 2 of the License, or 229 | (at your option) any later version. 230 | 231 | This program is distributed in the hope that it will be useful, 232 | but WITHOUT ANY WARRANTY; without even the implied warranty of 233 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 234 | GNU General Public License for more details. 235 | 236 | You should have received a copy of the GNU General Public License 237 | along with this program. If not, see 238 | 239 | See COPYRIGHT and LICENSE files for full copyright information. 240 | -------------------------------------------------------------------------------- /completions/_depthchargectl.bash: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | # depthcharge-tools depthchargectl bash completions 4 | # Copyright (C) 2020-2022 Alper Nebi Yasak 5 | # See COPYRIGHT and LICENSE files for full copyright information. 6 | 7 | _depthchargectl__file() { 8 | COMPREPLY+=($(compgen -f -- "$cur")) 9 | compopt -o filenames 10 | if [ "${#COMPREPLY[@]}" -eq 1 ]; then 11 | if [ -d "${COMPREPLY[0]}" ]; then 12 | compopt -o nospace 13 | COMPREPLY=("${COMPREPLY[0]}/") 14 | elif [ -f "${COMPREPLY[0]}" ]; then 15 | compopt +o nospace 16 | fi 17 | fi 18 | } 2>/dev/null 19 | 20 | _depthchargectl__timestamp() { 21 | local timestamp="$(date "+%s")" 22 | COMPREPLY+=($(compgen -W "$timestamp" -- "$cur")) 23 | } 2>/dev/null 24 | 25 | _depthchargectl__disk() { 26 | local disks="$(lsblk -o "PATH" -n -l)" 27 | COMPREPLY+=($(compgen -W "$disks" -- "$cur")) 28 | } 2>/dev/null 29 | 30 | _depthchargectl__root() { 31 | local root="$(findmnt --fstab -n -o SOURCE "/")" 32 | COMPREPLY+=($(compgen -W "$root" -- "$cur")) 33 | } 2>/dev/null 34 | 35 | _depthchargectl__boot() { 36 | local boot="$(findmnt --fstab -n -o SOURCE "/boot")" 37 | COMPREPLY+=($(compgen -W "$boot" -- "$cur")) 38 | } 2>/dev/null 39 | 40 | _depthchargectl__cmdline() { 41 | local cmdline="$(cat /proc/cmdline | sed -e 's/\(cros_secure\|kern_guid\)[^ ]* //g')" 42 | COMPREPLY+=($(compgen -W "$cmdline" -- "$cur")) 43 | } 2>/dev/null 44 | 45 | _depthchargectl__kernel() { 46 | if command -v _kernel_versions >/dev/null 2>/dev/null; then 47 | _kernel_versions 48 | else 49 | local script="from depthcharge_tools.utils.platform import installed_kernels" 50 | "$script;kernels = (k.release for k in installed_kernels());" 51 | "$script;print(*sorted(filter(None, kernels)));" 52 | COMPREPLY+=($(compgen -W "$(python3 -c "$script")" -- "$cur")) 53 | fi 54 | } 2>/dev/null 55 | 56 | _depthchargectl__boards() { 57 | # later 58 | local script="import re" 59 | script="$script;from depthcharge_tools import boards_ini" 60 | script="$script;boards = re.findall(\"codename = (.+)\", boards_ini)" 61 | script="$script;print(*sorted(boards))" 62 | COMPREPLY+=($(compgen -W "$(python3 -c "$script")" -- "$cur")) 63 | } 2>/dev/null 64 | 65 | _depthchargectl() { 66 | COMPREPLY=() 67 | local cur="${COMP_WORDS[COMP_CWORD]}" 68 | local prev="${COMP_WORDS[COMP_CWORD-1]}" 69 | local global_opts=(-h --help -V --version -v --verbose --tmpdir --root) 70 | local config_opts=( 71 | --config --board --images-dir 72 | --vboot-keyblock --vboot-public-key --vboot-private-key 73 | --kernel-cmdline --ignore-initramfs 74 | ) 75 | local cmds=(bless build config check list remove target write) 76 | 77 | case "$prev" in 78 | --root) _depthchargectl__root; _depthchargectl__disk; return ;; 79 | --root-mountpoint) _depthchargectl__file; return ;; 80 | --boot-mountpoint) _depthchargectl__file; return ;; 81 | --tmpdir) _depthchargectl__file; return ;; 82 | --config) _depthchargectl__file; return ;; 83 | --board) _depthchargectl__boards; return ;; 84 | --images-dir) _depthchargectl__file; return ;; 85 | --vboot-keyblock) _depthchargectl__file; return ;; 86 | --vboot-public-key) _depthchargectl__file; return ;; 87 | --vboot-private-key) _depthchargectl__file; return ;; 88 | --kernel-cmdline) _depthchargectl__cmdline; return ;; 89 | --ignore-initramfs) : ;; 90 | --zimage-initramfs-hack) COMPREPLY+=($(compgen -W "set-init-size pad-vmlinuz none" -- "$cur")) ;; 91 | --) ;; 92 | *) ;; 93 | esac 94 | 95 | local cmd 96 | for cmd in "${COMP_WORDS[@]}"; do 97 | case "$cmd" in 98 | bless) _depthchargectl_bless; break ;; 99 | build) _depthchargectl_build; break ;; 100 | config) _depthchargectl_config; break ;; 101 | check) _depthchargectl_check; break ;; 102 | list) _depthchargectl_list; break ;; 103 | remove) _depthchargectl_remove; break ;; 104 | target) _depthchargectl_target; break ;; 105 | write) _depthchargectl_write; break ;; 106 | *) cmd="" ;; 107 | esac 108 | done 109 | 110 | if [ -z "$cmd" ]; then 111 | COMPREPLY+=($(compgen -W "${cmds[*]}" -- "$cur")) 112 | COMPREPLY+=($(compgen -W "${global_opts[*]}" -- "$cur")) 113 | COMPREPLY+=($(compgen -W "${config_opts[*]}" -- "$cur")) 114 | fi 115 | } 116 | 117 | _depthchargectl_bless() { 118 | local opts=(--bad --oneshot -i --partno) 119 | case "$prev" in 120 | -i|--partno) return ;; 121 | *) _depthchargectl__disk ;; 122 | esac 123 | COMPREPLY+=($(compgen -W "${opts[*]}" -- "$cur")) 124 | COMPREPLY+=($(compgen -W "${global_opts[*]}" -- "$cur")) 125 | COMPREPLY+=($(compgen -W "${config_opts[*]}" -- "$cur")) 126 | } 127 | 128 | _depthchargectl_build() { 129 | local opts=( 130 | --description --root --compress --timestamp -o --output 131 | --kernel-release --kernel --initramfs --fdtdir --dtbs 132 | ) 133 | case "$prev" in 134 | --description) 135 | if [ -f /etc/os-release ]; then 136 | local name="$(. /etc/os-release; echo "$NAME")" 137 | COMPREPLY+=($(compgen -W "$name" -- "$cur")) 138 | fi 139 | return 140 | ;; 141 | --root) _depthchargectl__root; _depthchargectl__disk; return ;; 142 | --compress) 143 | local compress=(none lz4 lzma) 144 | COMPREPLY+=($(compgen -W "${compress[*]}" -- "$cur")) 145 | return 146 | ;; 147 | --timestamp) _depthchargectl__timestamp; return;; 148 | -o|--output) _depthchargectl__file; return ;; 149 | --kernel-release) _depthchargectl__kernel; return ;; 150 | --kernel) _depthchargectl__file; return ;; 151 | --initramfs) _depthchargectl__file; return ;; 152 | --fdtdir) _depthchargectl__file; return ;; 153 | --dtbs) _depthchargectl__file; return ;; 154 | *) _depthchargectl__kernel;; 155 | esac 156 | COMPREPLY+=($(compgen -W "${opts[*]}" -- "$cur")) 157 | COMPREPLY+=($(compgen -W "${global_opts[*]}" -- "$cur")) 158 | COMPREPLY+=($(compgen -W "${config_opts[*]}" -- "$cur")) 159 | } 160 | 161 | _depthchargectl_config() { 162 | local opts=(--section --default) 163 | case "$prev" in 164 | --section) return ;; 165 | --default) return ;; 166 | *) ;; 167 | esac 168 | COMPREPLY+=($(compgen -W "${opts[*]}" -- "$cur")) 169 | COMPREPLY+=($(compgen -W "${global_opts[*]}" -- "$cur")) 170 | COMPREPLY+=($(compgen -W "${config_opts[*]}" -- "$cur")) 171 | } 172 | 173 | _depthchargectl_check() { 174 | _depthchargectl__file 175 | COMPREPLY+=($(compgen -W "${global_opts[*]}" -- "$cur")) 176 | COMPREPLY+=($(compgen -W "${config_opts[*]}" -- "$cur")) 177 | } 178 | 179 | _depthchargectl_list() { 180 | local opts=(-a --all-disks -c --count -n --noheadings -o --output) 181 | local outputs=(A ATTRIBUTE S SUCCESSFUL T TRIES P PRIORITY PATH DISK DISKPATH PARTNO SIZE) 182 | case "$prev" in 183 | -o|--output) 184 | compopt -o nospace 185 | case "$cur" in 186 | *,) COMPREPLY+=($(compgen -W "${outputs[*]}" -P "$cur" -- "")) ;; 187 | *,*) COMPREPLY+=($(compgen -W "${outputs[*]}" -P "${cur%,*}," -- "${cur##*,}")) ;; 188 | *) COMPREPLY+=($(compgen -W "${outputs[*]}" -- "$cur")) ;; 189 | esac 190 | ;; 191 | *) 192 | _depthchargectl__disk 193 | COMPREPLY+=($(compgen -W "${opts[*]}" -- "$cur")) 194 | COMPREPLY+=($(compgen -W "${global_opts[*]}" -- "$cur")) 195 | COMPREPLY+=($(compgen -W "${config_opts[*]}" -- "$cur")) 196 | ;; 197 | esac 198 | } 199 | 200 | _depthchargectl_remove() { 201 | local opts=(-f --force) 202 | _depthchargectl__file 203 | _depthchargectl__kernel 204 | COMPREPLY+=($(compgen -W "${opts[*]}" -- "$cur")) 205 | COMPREPLY+=($(compgen -W "${global_opts[*]}" -- "$cur")) 206 | COMPREPLY+=($(compgen -W "${config_opts[*]}" -- "$cur")) 207 | } 208 | 209 | _depthchargectl_target() { 210 | local opts=(-s --min-size --allow-current -a --all-disks) 211 | local sizes=(8M 16M 32M 64M 128M 256M 512M) 212 | case "$prev" in 213 | -s|--min-size) 214 | COMPREPLY+=($(compgen -W "${sizes[*]}" -- "$cur")) 215 | ;; 216 | *) 217 | _depthchargectl__disk 218 | COMPREPLY+=($(compgen -W "${opts[*]}" -- "$cur")) 219 | COMPREPLY+=($(compgen -W "${global_opts[*]}" -- "$cur")) 220 | COMPREPLY+=($(compgen -W "${config_opts[*]}" -- "$cur")) 221 | ;; 222 | esac 223 | } 224 | 225 | _depthchargectl_write() { 226 | local opts=(-f --force -t --target --no-prioritize --allow-current) 227 | case "$prev" in 228 | -t|--target) 229 | _depthchargectl__disk 230 | ;; 231 | *) 232 | _depthchargectl__kernel 233 | _depthchargectl__file 234 | COMPREPLY+=($(compgen -W "${opts[*]}" -- "$cur")) 235 | COMPREPLY+=($(compgen -W "${global_opts[*]}" -- "$cur")) 236 | COMPREPLY+=($(compgen -W "${config_opts[*]}" -- "$cur")) 237 | ;; 238 | esac 239 | } 240 | 241 | complete -F _depthchargectl depthchargectl 242 | 243 | # vim: filetype=sh 244 | -------------------------------------------------------------------------------- /completions/_depthchargectl.zsh: -------------------------------------------------------------------------------- 1 | #compdef depthchargectl 2 | # SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | # depthcharge-tools depthchargectl zsh completions 5 | # Copyright (C) 2020-2022 Alper Nebi Yasak 6 | # See COPYRIGHT and LICENSE files for full copyright information. 7 | 8 | function _depthchargectl { 9 | _arguments -C \ 10 | {-h,--help}'[Show a help message.]' \ 11 | {-v,--verbose}'[Print more detailed output.]' \ 12 | {-V,--version}'[Print program version.]' \ 13 | --tmpdir'[Directory to keep temporary files.]:temp dir:_directories' \ 14 | --root'[Root device or mountpoint of the system to work on]:root device:{_depthchargectl__root; _depthchargectl__disk}' \ 15 | --root-mountpoint'[Root mountpoint of the system to work on]:root mountpoint:_directories' \ 16 | --boot-mountpoint'[Boot mountpoint of the system to work on]:boot mountpoint:_directories' \ 17 | --config'[Additional configuration file to read]:config file:_files' \ 18 | --board'[Assume running on the specified board]:board codenames:{_depthchargectl__board;}' \ 19 | --images-dir'[Directory to store built images]:images dir:_directories' \ 20 | --vboot-keyblock'[Keyblock file to include in images]:keyblock file:_files' \ 21 | --vboot-public-key'[Public key file to verify images]:vbpubk file:_files' \ 22 | --vboot-private-key'[Private key file to include in images]:vbprivk file:_files' \ 23 | --kernel-cmdline'[Command line options for the kernel]:kernel cmdline:{_depthchargectl__cmdline;}' \ 24 | --ignore-initramfs'[Do not include initramfs in images]' \ 25 | --zimage-initramfs-hack'[Initramfs support hack choice for zimage format]:zimage hack:(set-init-size pad-vmlinuz none)' \ 26 | '1:command:(bless build config check list remove target write)' \ 27 | '*::arg:->args' \ 28 | ; 29 | 30 | case "$state:$line[1]" in 31 | args:bless) 32 | _arguments -S \ 33 | --bad'[Set the partition as unbootable]' \ 34 | --oneshot'[Set the partition to be tried once]' \ 35 | {-i,--partno}'[Partition number in the given disk image]:number:()' \ 36 | ':disk or partition:{_depthchargectl__disk}' \ 37 | ; 38 | ;; 39 | args:build) 40 | _arguments -S \ 41 | --description'[Human-readable description for the image]:image description:($(source /etc/os-release; echo "$NAME"))' \ 42 | --root'[Root device to add to kernel cmdline]:root device:{_depthchargectl__root; _depthchargectl__disk}' \ 43 | --compress'[Compression types to attempt]:compress:(none lz4 lzma)' \ 44 | --timestamp'[Build timestamp for the image]:timestamp:($(date "+%s"))' \ 45 | {-o,--output}'[Output image to path instead of storing in images-dir]:output path:_files' \ 46 | --kernel-release'[Release name for the kernel used in image name]:kernel release:{_depthchargectl__kernel;}' \ 47 | --kernel'[Kernel executable]:kernel:_files' \ 48 | --initramfs'[Ramdisk image]:*:initramfs:_files' \ 49 | --fdtdir'[Directory to search device-tree binaries for the board]:fdtdir:_directories' \ 50 | --dtbs'[Device-tree binary files to use instead of searching fdtdir]:*:dtb files:_files' \ 51 | ':kernel version:{_depthchargectl__kernel}' \ 52 | ; 53 | ;; 54 | args:config) 55 | _arguments -S \ 56 | --section'[Config section to work on.]' \ 57 | --default'[Value to return if key does not exist in section.]' \ 58 | ':config key:' \ 59 | ; 60 | ;; 61 | args:check) 62 | _arguments -S \ 63 | ':image file:_files' \ 64 | ; 65 | ;; 66 | args:list) 67 | local outputspec='{_values -s , "description" "A" "ATTRIBUTE" "S" "SUCCESSFUL" "T" "TRIES" "P" "PRIORITY" "PATH" "DISKPATH" "DISK" "PARTNO" "SIZE"}' 68 | _arguments -S \ 69 | {-n,--noheadings}'[Do not print column headings.]' \ 70 | {-a,--all-disks}'[List partitions on all disks.]' \ 71 | {-c,--count}'[Print only the count of partitions.]' \ 72 | {-o,--output}'[Comma separated list of columns to output.]:columns:'"$outputspec" \ 73 | '*::disk or partition:{_depthchargectl__disk}' \ 74 | ; 75 | ;; 76 | args:remove) 77 | _arguments -S \ 78 | {-f,--force}'[Allow disabling the current partition.]' \ 79 | '::kernel version or image file:{_depthchargectl__kernel; _files}' \ 80 | ; 81 | ;; 82 | args:target) 83 | _arguments -S \ 84 | {-s,--min-size}'[Target partitions larger than this size.]:bytes:(8M 16M 32M 64M 128M 256M 512M)' \ 85 | --allow-current'[Allow targeting the currently booted part.]' \ 86 | {-a,--all-disks}'[Target partitions on all disks.]' \ 87 | '*::disk or partition:{_depthchargectl__disk}' \ 88 | ; 89 | ;; 90 | args:write) 91 | _arguments -S \ 92 | {-f,--force}'[Write image even if it cannot be verified.]' \ 93 | {-t,--target}'[Specify a disk or partition to write to.]:disk or partition:{_depthchargectl__disk}' \ 94 | --no-prioritize'[Do not set any flags on the partition]' \ 95 | --allow-current'[Allow overwriting the current partition]' \ 96 | '::kernel version or image file:{_depthchargectl__kernel; _files}' \ 97 | ; 98 | ;; 99 | *) : ;; 100 | esac 101 | 102 | } 103 | 104 | function _depthchargectl__kernel { 105 | if command -v linux-version >/dev/null 2>/dev/null; then 106 | local kversions=($(linux-version list)) 107 | _describe 'kernel version' kversions 108 | else 109 | local script=( 110 | 'from depthcharge_tools.utils.platform import installed_kernels;' 111 | 'kernels = (k.release for k in installed_kernels());' 112 | 'print(*sorted(filter(None, kernels)));' 113 | ) 114 | local kversions=($(python3 -c "$script")) 115 | _describe 'kernel version' kversions 116 | fi 117 | } 2>/dev/null 118 | 119 | function _depthchargectl__disk { 120 | local disks=($(lsblk -o "PATH" -n -l)) 2>/dev/null 121 | _describe 'disk or partition' disks 122 | } 2>/dev/null 123 | 124 | function _depthchargectl__board { 125 | local script=( 126 | 'import re;' 127 | 'from depthcharge_tools import boards_ini;' 128 | 'boards = re.findall("codename = (.+)", boards_ini);' 129 | 'print(*sorted(boards));' 130 | ) 131 | local boards=($(python3 -c "$script")) 132 | _describe 'board codenames' boards 133 | } 2>/dev/null 134 | 135 | function _depthchargectl__cmdline { 136 | local cmdline=($(cat /proc/cmdline | sed -e 's/\(cros_secure\|kern_guid\)[^ ]* //g')) 137 | _describe 'kernel cmdline' cmdline 138 | } 2>/dev/null 139 | 140 | function _depthchargectl__root { 141 | local root=($(findmnt --fstab -n -o SOURCE "/")) 142 | _describe root root 143 | } 2>/dev/null 144 | 145 | function _depthchargectl__boot { 146 | local boot=($(findmnt --fstab -n -o SOURCE "/boot")) 147 | _describe boot boot 148 | } 2>/dev/null 149 | 150 | _depthchargectl "$@" 151 | -------------------------------------------------------------------------------- /completions/_mkdepthcharge.bash: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | # depthcharge-tools mkdepthcharge bash completions 4 | # Copyright (C) 2020-2022 Alper Nebi Yasak 5 | # See COPYRIGHT and LICENSE files for full copyright information. 6 | 7 | _mkdepthcharge__file() { 8 | COMPREPLY+=($(compgen -f -- "$cur")) 9 | compopt -o filenames 10 | if [ "${#COMPREPLY[@]}" -eq 1 ]; then 11 | if [ -d "${COMPREPLY[0]}" ]; then 12 | compopt -o nospace 13 | COMPREPLY=("${COMPREPLY[0]}/") 14 | elif [ -f "${COMPREPLY[0]}" ]; then 15 | compopt +o nospace 16 | fi 17 | fi 18 | } 2>/dev/null 19 | 20 | _mkdepthcharge__cmdline() { 21 | cmdline="$(cat /proc/cmdline | sed -e 's/\(cros_secure\|kern_guid\)[^ ]* //g')" 22 | COMPREPLY+=($(compgen -W "$cmdline" -- "$cur")) 23 | } 2>/dev/null 24 | 25 | _mkdepthcharge() { 26 | COMPREPLY=() 27 | local cur="${COMP_WORDS[COMP_CWORD]}" 28 | local prev="${COMP_WORDS[COMP_CWORD-1]}" 29 | local opts=( 30 | -h --help -V --version -v --verbose 31 | -d --vmlinuz -i --initramfs -b --dtbs 32 | -o --output --tmpdir -A --arch --format 33 | -C --compress -n --name --kernel-start 34 | --ramdisk-load-address --patch-dtbs --no-patch-dtbs 35 | --pad-vmlinuz --no-pad-vmlinuz 36 | --set-init-size --no-set-init-size 37 | -c --cmdline --kern-guid --no-kern-guid --bootloader 38 | --keydir --keyblock --signprivate --signpubkey 39 | ) 40 | 41 | case "$prev" in 42 | -d|--vmlinuz) _mkdepthcharge__file ;; 43 | -i|--initramfs) _mkdepthcharge__file ;; 44 | -b|--dtbs) _mkdepthcharge__file ;; 45 | -o|--output) _mkdepthcharge__file ;; 46 | -A|--arch) COMPREPLY+=($(compgen -W "arm arm64 aarch64 x86 x86_64 amd64" -- "$cur")) ;; 47 | --format) COMPREPLY+=($(compgen -W "fit zimage" -- "$cur")) ;; 48 | -C|--compress) COMPREPLY+=($(compgen -W "none lz4 lzma" -- "$cur")) ;; 49 | -n|--name) 50 | if [ -f /etc/os-release ]; then 51 | local name="$(. /etc/os-release; echo "$NAME")" 52 | COMPREPLY+=($(compgen -W "$name" -- "$cur")) 53 | fi 54 | ;; 55 | --kernel-start) : ;; 56 | --ramdisk-load-address) : ;; 57 | -c|--cmdline) _mkdepthcharge__cmdline ;; 58 | --tmpdir) _mkdepthcharge__file ;; 59 | --bootloader) _mkdepthcharge__file ;; 60 | --keydir) _mkdepthcharge__file ;; 61 | --keyblock) _mkdepthcharge__file ;; 62 | --signprivate) _mkdepthcharge__file ;; 63 | --signpubkey) _mkdepthcharge__file ;; 64 | --) _mkdepthcharge__file ;; 65 | *) 66 | COMPREPLY+=($(compgen -W "${opts[*]}" -- "$cur")) 67 | _mkdepthcharge__file 68 | ;; 69 | esac 70 | } 71 | 72 | complete -F _mkdepthcharge mkdepthcharge 73 | 74 | # vim: filetype=sh 75 | -------------------------------------------------------------------------------- /completions/_mkdepthcharge.zsh: -------------------------------------------------------------------------------- 1 | #compdef mkdepthcharge 2 | # SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | # depthcharge-tools mkdepthcharge zsh completions 5 | # Copyright (C) 2020-2022 Alper Nebi Yasak 6 | # See COPYRIGHT and LICENSE files for full copyright information. 7 | 8 | function _mkdepthcharge { 9 | _arguments -S \ 10 | {-d,--vmlinuz}'[Kernel executable]:vmlinuz file:_files' \ 11 | {-i,--initramfs}'[Ramdisk image]:*:initrd files:_files' \ 12 | {-b,--dtbs}'[Device-tree binary files]:*:dtbs files:_files' \ 13 | {-h,--help}'[Show a help message.]' \ 14 | {-v,--verbose}'[Print more detailed output.]' \ 15 | {-V,--version}'[Print program version.]' \ 16 | --tmpdir'[Directory to keep temporary files.]:temp dir:_directories' \ 17 | --kernel-start'[Start of depthcharge kernel buffer in memory.]:kernel start:_numbers' \ 18 | {-o,--output}'[Write resulting image to FILE.]:output:_files' \ 19 | {-A,--arch}'[Architecture to build for.]:arch:(arm arm64 aarch64 x86 x86_64 amd64)' \ 20 | --format'[Kernel image format to use.]:format:(fit zimage)' \ 21 | {-C,--compress}'[Compress vmlinuz with lz4 or lzma.]:compression:(none lz4 lzma)' \ 22 | {-n,--name}'[Description of vmlinuz to put in the FIT.]:description:($(source /etc/os-release; echo "$NAME"))' \ 23 | --ramdisk-load-address'[Add load address to FIT ramdisk image section.]:ramdisk load address:_numbers' \ 24 | --patch-dtbs'[Add linux,initrd properties to device-tree binary files.]' \ 25 | --no-patch-dtbs'[Do not add linux,initrd properties to device-tree binary files.]' \ 26 | --pad-vmlinuz'[Pad the vmlinuz file for safe decompression]' \ 27 | --no-pad-vmlinuz'[Do not pad the vmlinuz file for safe decompression]' \ 28 | --set-init-size'[Set init-size boot param for safe decompression]' \ 29 | --no-set-init-size'[Do not set init-size boot param for safe decompression]' \ 30 | '*'{-c,--cmdline}'[Command-line parameters for the kernel.]:*:kernel cmdline:{_mkdepthcharge__cmdline}' \ 31 | --kern-guid'[Prepend kern_guid=%U to the cmdline.]' \ 32 | --no-kern-guid'[Do not prepend kern_guid=%U to the cmdline.]' \ 33 | --bootloader'[Bootloader stub binary to use.]:bootloader file:_files' \ 34 | --keydir'[Directory containing vboot keys to use.]:keys dir:_directories' \ 35 | --keyblock'[The key block file (.keyblock).]:keyblock file:_files' \ 36 | --signprivate'[Private key (.vbprivk) to sign the image.]:vbprivk file:_files' \ 37 | --signpubkey'[Public key (.vbpubk) to verify the image.]:vbpubk file:_files' \ 38 | ':vmlinuz file:_files' \ 39 | '*:initrd or dtb files:_files' \ 40 | ; 41 | } 42 | 43 | function _mkdepthcharge__cmdline { 44 | local cmdline=($(cat /proc/cmdline | sed -e 's/\(cros_secure\|kern_guid\)[^ ]* //g')) 45 | _describe 'kernel cmdline' cmdline 46 | } 2>/dev/null 47 | 48 | _mkdepthcharge "$@" 49 | -------------------------------------------------------------------------------- /depthcharge_tools/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | # depthcharge-tools __init__.py 5 | # Copyright (C) 2020-2021 Alper Nebi Yasak 6 | # See COPYRIGHT and LICENSE files for full copyright information. 7 | 8 | import glob 9 | import logging 10 | import pathlib 11 | import pkg_resources 12 | import re 13 | import subprocess 14 | 15 | logger = logging.getLogger(__name__) 16 | logger.addHandler(logging.NullHandler()) 17 | 18 | 19 | def get_version(): 20 | version = None 21 | pkg_path = pkg_resources.resource_filename(__name__, '') 22 | pkg_path = pathlib.Path(pkg_path).resolve() 23 | 24 | try: 25 | self = pkg_resources.require(__name__)[0] 26 | version = self.version 27 | 28 | except pkg_resources.DistributionNotFound: 29 | setup_py = pkg_path.parent / "setup.py" 30 | if setup_py.exists(): 31 | version = re.findall( 32 | 'version=(\'.+\'|".+"),', 33 | setup_py.read_text(), 34 | )[0].strip('"\'') 35 | 36 | if (pkg_path.parent / ".git").exists(): 37 | proc = subprocess.run( 38 | ["git", "-C", pkg_path, "describe"], 39 | stdout=subprocess.PIPE, 40 | encoding="utf-8", 41 | check=False, 42 | ) 43 | if proc.returncode == 0: 44 | tag, *local = proc.stdout.split("-") 45 | 46 | if local: 47 | version = "{}+{}".format(tag, ".".join(local)) 48 | else: 49 | version = tag 50 | 51 | if version is not None: 52 | return pkg_resources.parse_version(version) 53 | 54 | __version__ = get_version() 55 | 56 | config_ini = pkg_resources.resource_string(__name__, "config.ini") 57 | config_ini = config_ini.decode("utf-8") 58 | 59 | boards_ini = pkg_resources.resource_string(__name__, "boards.ini") 60 | boards_ini = boards_ini.decode("utf-8") 61 | 62 | config_files = [ 63 | *glob.glob("/etc/depthcharge-tools/config"), 64 | *glob.glob("/etc/depthcharge-tools/config.d/*"), 65 | ] 66 | 67 | 68 | -------------------------------------------------------------------------------- /depthcharge_tools/config.ini: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | # depthcharge-tools configuration 4 | # Copyright (C) 2021-2023 Alper Nebi Yasak 5 | # See COPYRIGHT and LICENSE files for full copyright information. 6 | 7 | [depthcharge-tools] 8 | enable-system-hooks = True 9 | #vboot-keyblock = /etc/depthcharge-tools/kernel.keyblock 10 | #vboot-private-key = /etc/depthcharge-tools/kernel_data_key.vbprivk 11 | #vboot-public-key = /etc/depthcharge-tools/kernel_subkey.vbpubk 12 | 13 | [depthchargectl] 14 | #board = 15 | ignore-initramfs = False 16 | images-dir = /boot/depthcharge 17 | #kernel-cmdline = console=tty0 quiet splash 18 | zimage-initramfs-hack = set-init-size 19 | -------------------------------------------------------------------------------- /depthcharge_tools/depthchargectl/__main__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | # depthcharge-tools depthchargectl program 5 | # Copyright (C) 2021 Alper Nebi Yasak 6 | # See COPYRIGHT and LICENSE files for full copyright information. 7 | 8 | from depthcharge_tools.depthchargectl import depthchargectl 9 | 10 | if __name__ == "__main__": 11 | depthchargectl.main() 12 | -------------------------------------------------------------------------------- /depthcharge_tools/depthchargectl/_bless.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | # depthcharge-tools depthchargectl bless subcommand 5 | # Copyright (C) 2020-2022 Alper Nebi Yasak 6 | # See COPYRIGHT and LICENSE files for full copyright information. 7 | 8 | import argparse 9 | import logging 10 | import subprocess 11 | 12 | from depthcharge_tools import __version__ 13 | from depthcharge_tools.utils.argparse import ( 14 | Command, 15 | Argument, 16 | Group, 17 | CommandExit, 18 | ) 19 | from depthcharge_tools.utils.os import ( 20 | Disk, 21 | Partition, 22 | CrosPartition, 23 | ) 24 | from depthcharge_tools.utils.platform import ( 25 | is_cros_boot, 26 | ) 27 | 28 | from depthcharge_tools.depthchargectl import depthchargectl 29 | 30 | 31 | @depthchargectl.subcommand("bless") 32 | class depthchargectl_bless( 33 | depthchargectl, 34 | prog="depthchargectl bless", 35 | usage="%(prog)s [options] [DISK | PARTITION]", 36 | add_help=False, 37 | ): 38 | """Set the active or given partition as successfully booted.""" 39 | 40 | _logger = depthchargectl._logger.getChild("bless") 41 | config_section = "depthchargectl/bless" 42 | 43 | @depthchargectl.board.copy() 44 | def board(self, codename=""): 45 | """Assume we're running on the specified board""" 46 | # We can bless partitions without knowing the board. 47 | try: 48 | return super().board 49 | except Exception as err: 50 | self.logger.warning(err) 51 | return None 52 | 53 | @Group 54 | def positionals(self): 55 | """Positional arguments""" 56 | if self.disk is not None and self.partition is not None: 57 | raise ValueError( 58 | "Disk and partition arguments are mutually exclusive." 59 | ) 60 | 61 | device = self.disk or self.partition 62 | 63 | if isinstance(device, str): 64 | sys_device = self.diskinfo.evaluate(device) 65 | 66 | if sys_device is not None: 67 | self.logger.info( 68 | "Using argument '{}' as a block device." 69 | .format(device) 70 | ) 71 | device = sys_device 72 | 73 | else: 74 | self.logger.info( 75 | "Using argument '{}' as a disk image." 76 | .format(device) 77 | ) 78 | device = Disk(device) 79 | 80 | if isinstance(device, Disk): 81 | if self.partno is None: 82 | raise ValueError( 83 | "Partno argument is required for disks." 84 | ) 85 | partition = device.partition(self.partno) 86 | 87 | elif isinstance(device, Partition): 88 | if self.partno is not None and self.partno != device.partno: 89 | raise ValueError( 90 | "Partition and partno arguments are mutually exclusive." 91 | ) 92 | partition = device 93 | 94 | elif device is None: 95 | self.logger.info( 96 | "No partition given, defaulting to currently booted one." 97 | ) 98 | partition = self.diskinfo.by_kern_guid() 99 | 100 | if partition is None: 101 | if is_cros_boot(): 102 | raise ValueError( 103 | "Couldn't figure out the currently booted partition." 104 | ) 105 | else: 106 | raise ValueError( 107 | "A disk or partition argument is required when not " 108 | "booted with depthcharge." 109 | ) 110 | 111 | self.logger.info( 112 | "Working on partition '{}'." 113 | .format(partition) 114 | ) 115 | 116 | try: 117 | cros_partitions = partition.disk.cros_partitions() 118 | except subprocess.CalledProcessError as err: 119 | self.logger.debug( 120 | err, 121 | exc_info=self.logger.isEnabledFor(logging.DEBUG), 122 | ) 123 | raise ValueError( 124 | "Couldn't get partitions for disk '{}'." 125 | .format(partition.disk) 126 | ) from err 127 | 128 | if partition not in cros_partitions: 129 | raise ValueError( 130 | "Partition '{}' is not a ChromeOS Kernel partition" 131 | .format(partition) 132 | ) 133 | 134 | partition = CrosPartition(partition) 135 | self.partition = partition 136 | self.disk = partition.disk 137 | self.partno = partition.partno 138 | 139 | @positionals.add 140 | @Argument(nargs=0) 141 | def disk(self, disk=None): 142 | """Disk image to manage partitions of""" 143 | return disk 144 | 145 | @positionals.add 146 | @Argument 147 | def partition(self, partition=None): 148 | """ChromeOS Kernel partition device to manage""" 149 | return partition 150 | 151 | @Group 152 | def options(self): 153 | """Options""" 154 | 155 | @options.add 156 | @Argument("-i", "--partno", nargs=1) 157 | def partno(self, number=None): 158 | """Partition number in the given disk image""" 159 | try: 160 | if number is not None: 161 | number = int(number) 162 | except: 163 | raise TypeError( 164 | "Partition number must be a positive integer." 165 | ) 166 | 167 | if number is not None and not number > 0: 168 | raise ValueError( 169 | "Partition number must be a positive integer." 170 | ) 171 | 172 | return number 173 | 174 | @options.add 175 | @Argument("--bad", bad=True) 176 | def bad(self, bad=False): 177 | """Set the partition as unbootable""" 178 | return bad 179 | 180 | @options.add 181 | @Argument("--oneshot", oneshot=True) 182 | def oneshot(self, oneshot=False): 183 | """Set the partition to be tried once""" 184 | return oneshot 185 | 186 | def __call__(self): 187 | if self.bad == False: 188 | try: 189 | self.partition.tries = 1 190 | except subprocess.CalledProcessError as err: 191 | raise CommandExit( 192 | "Failed to set remaining tries for partition '{}'." 193 | .format(self.partition) 194 | ) from err 195 | 196 | if self.oneshot == False: 197 | try: 198 | self.partition.successful = 1 199 | except subprocess.CalledProcessError as err: 200 | raise CommandExit( 201 | "Failed to set success flag for partition '{}'." 202 | .format(self.partition) 203 | ) from err 204 | 205 | self.logger.warning( 206 | "Set partition '{}' as successfully booted." 207 | .format(self.partition) 208 | ) 209 | 210 | else: 211 | try: 212 | self.partition.successful = 0 213 | except subprocess.CalledProcessError as err: 214 | raise CommandExit( 215 | "Failed to unset successful flag for partition '{}'." 216 | .format(self.partition) 217 | ) from err 218 | 219 | self.logger.warning( 220 | "Set partition '{}' as not yet successfully booted." 221 | .format(self.partition) 222 | ) 223 | 224 | try: 225 | self.partition.prioritize() 226 | except subprocess.CalledProcessError as err: 227 | raise CommandExit( 228 | "Failed to prioritize partition '{}'." 229 | .format(self.partition) 230 | ) from err 231 | 232 | self.logger.info( 233 | "Set partition '{}' as the highest-priority bootable part." 234 | .format(self.partition) 235 | ) 236 | 237 | else: 238 | try: 239 | self.partition.attribute = 0x000 240 | except subprocess.CalledProcessError as err: 241 | raise CommandExit( 242 | "Failed to zero attributes for partition '{}'." 243 | .format(self.partition) 244 | ) from err 245 | 246 | self.logger.warning( 247 | "Set partition '{}' as a zero-priority unbootable part." 248 | .format(self.partition) 249 | ) 250 | 251 | global_options = depthchargectl.global_options 252 | config_options = depthchargectl.config_options 253 | -------------------------------------------------------------------------------- /depthcharge_tools/depthchargectl/_check.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | # depthcharge-tools depthchargectl check subcommand 5 | # Copyright (C) 2020-2023 Alper Nebi Yasak 6 | # See COPYRIGHT and LICENSE files for full copyright information. 7 | 8 | import argparse 9 | import logging 10 | 11 | from pathlib import Path 12 | 13 | from depthcharge_tools import __version__ 14 | from depthcharge_tools.utils.argparse import ( 15 | Command, 16 | Argument, 17 | Group, 18 | CommandExit, 19 | ) 20 | from depthcharge_tools.utils.subprocess import ( 21 | fdtget, 22 | vbutil_kernel, 23 | ) 24 | 25 | from depthcharge_tools.depthchargectl import depthchargectl 26 | 27 | 28 | class SizeTooBigError(CommandExit): 29 | def __init__(self, image, image_size, max_size): 30 | message = ( 31 | "Image '{}' ({} bytes) must be smaller than {} bytes." 32 | .format(image, image_size, max_size) 33 | ) 34 | 35 | self.image = image 36 | self.image_size = image_size 37 | self.max_size = max_size 38 | super().__init__(output=False, returncode=3, message=message) 39 | 40 | 41 | class NotADepthchargeImageError(CommandExit): 42 | def __init__(self, image): 43 | message = ( 44 | "Image '{}' is not a depthcharge image." 45 | .format(image) 46 | ) 47 | 48 | self.image = image 49 | super().__init__(output=False, returncode=4, message=message) 50 | 51 | 52 | class VbootSignatureError(CommandExit): 53 | def __init__(self, image): 54 | message = ( 55 | "Depthcharge image '{}' is not signed by the configured keys." 56 | .format(image) 57 | ) 58 | 59 | self.image = image 60 | super().__init__(output=False, returncode=5, message=message) 61 | 62 | 63 | class ImageFormatError(CommandExit): 64 | def __init__(self, image, board_format): 65 | message = ( 66 | "Image '{}' must be in '{}' format." 67 | .format(image, board_format) 68 | ) 69 | 70 | self.image = image 71 | self.board_format = board_format 72 | super().__init__(output=False, returncode=6, message=message) 73 | 74 | 75 | class MissingDTBError(CommandExit): 76 | def __init__(self, image, compat): 77 | message = ( 78 | "Image '{}' must have a device-tree binary compatible with pattern '{}'." 79 | .format(image, compat) 80 | ) 81 | 82 | self.image = image 83 | self.compat = compat 84 | super().__init__(output=False, returncode=7, message=message) 85 | 86 | 87 | @depthchargectl.subcommand("check") 88 | class depthchargectl_check( 89 | depthchargectl, 90 | prog="depthchargectl check", 91 | usage = "%(prog)s [options] IMAGE", 92 | add_help=False, 93 | ): 94 | """Check if a depthcharge image can be booted.""" 95 | 96 | _logger = depthchargectl._logger.getChild("check") 97 | config_section = "depthchargectl/check" 98 | 99 | @Group 100 | def positionals(self): 101 | """Positional arguments""" 102 | 103 | @positionals.add 104 | @Argument 105 | def image(self, image): 106 | """Depthcharge image to check validity of.""" 107 | image = Path(image) 108 | 109 | if not image.is_file(): 110 | raise ValueError("Image argument must be a file") 111 | 112 | return image 113 | 114 | @depthchargectl.board.copy() 115 | def board(self, codename=""): 116 | """Assume we're running on the specified board""" 117 | board = super().board 118 | 119 | if board is None: 120 | raise ValueError( 121 | "Cannot check depthcharge images when no board is specified.", 122 | ) 123 | 124 | return board 125 | 126 | @depthchargectl.zimage_initramfs_hack.copy() 127 | def zimage_initramfs_hack(self, hack=None): 128 | hack = super().zimage_initramfs_hack 129 | 130 | if hack not in (None, "set-init-size", "pad-vmlinuz"): 131 | raise ValueError( 132 | "Unknown zimage initramfs support hack '{}'." 133 | .format(hack) 134 | ) 135 | 136 | return hack 137 | 138 | 139 | def __call__(self): 140 | image = self.image 141 | 142 | self.logger.warning( 143 | "Verifying depthcharge image for board '{}' ('{}')." 144 | .format(self.board.name, self.board.codename) 145 | ) 146 | 147 | self.logger.info("Checking if image fits into size limit.") 148 | image_size = image.stat().st_size 149 | if image_size > self.board.image_max_size: 150 | raise SizeTooBigError( 151 | image, 152 | image_size, 153 | self.board.image_max_size, 154 | ) 155 | 156 | self.logger.info("Checking depthcharge image validity.") 157 | if vbutil_kernel( 158 | "--verify", image, 159 | check=False, 160 | ).returncode != 0: 161 | raise NotADepthchargeImageError(image) 162 | 163 | self.logger.info("Checking depthcharge image signatures.") 164 | if self.vboot_public_key is not None: 165 | if vbutil_kernel( 166 | "--verify", image, 167 | "--signpubkey", self.vboot_public_key, 168 | check=False, 169 | ).returncode != 0: 170 | raise VbootSignatureError(image) 171 | 172 | itb = self.tmpdir / "{}.itb".format(image.name) 173 | vbutil_kernel( 174 | "--get-vmlinuz", image, 175 | "--vmlinuz-out", itb, 176 | check=False, 177 | ) 178 | 179 | if self.board.image_format == "fit": 180 | self.logger.info("Checking FIT image format.") 181 | nodes = fdtget.subnodes(itb) 182 | if "images" not in nodes and "configurations" not in nodes: 183 | raise ImageFormatError(image, self.board.image_format) 184 | 185 | def is_compatible(dt_file, conf_path): 186 | return any( 187 | self.board.dt_compatible.fullmatch(compat) 188 | for compat in fdtget.get( 189 | dt_file, conf_path, "compatible", default="", 190 | ).split() 191 | ) 192 | 193 | self.logger.info("Checking included DTB binaries.") 194 | for conf in fdtget.subnodes(itb, "/configurations"): 195 | conf_path = "/configurations/{}".format(conf) 196 | if is_compatible(itb, conf_path): 197 | break 198 | 199 | dtb = fdtget.get(itb, conf_path, "fdt") 200 | dtb_path = "/images/{}".format(dtb) 201 | dtb_data = fdtget.get(itb, dtb_path, "data", type=bytes) 202 | dtb_file = self.tmpdir / "{}.dtb".format(conf) 203 | dtb_file.write_bytes(dtb_data) 204 | 205 | if is_compatible(dtb_file, "/"): 206 | break 207 | else: 208 | raise MissingDTBError( 209 | image, self.board.dt_compatible.pattern, 210 | ) 211 | 212 | self.logger.warning( 213 | "This command is incomplete, the image might be unbootable " 214 | "despite passing currently implemented checks." 215 | ) 216 | 217 | global_options = depthchargectl.global_options 218 | config_options = depthchargectl.config_options 219 | -------------------------------------------------------------------------------- /depthcharge_tools/depthchargectl/_config.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | # depthcharge-tools depthchargectl config subcommand 5 | # Copyright (C) 2021-2022 Alper Nebi Yasak 6 | # See COPYRIGHT and LICENSE files for full copyright information. 7 | 8 | import argparse 9 | import logging 10 | 11 | from depthcharge_tools import __version__ 12 | from depthcharge_tools.utils.argparse import ( 13 | Command, 14 | Argument, 15 | Group, 16 | CommandExit, 17 | ) 18 | 19 | from depthcharge_tools.depthchargectl import depthchargectl 20 | 21 | 22 | @depthchargectl.subcommand("config") 23 | class depthchargectl_config( 24 | depthchargectl, 25 | prog="depthchargectl config", 26 | usage="%(prog)s [options] KEY", 27 | add_help=False, 28 | ): 29 | """Get depthchargectl configuration values.""" 30 | 31 | _logger = depthchargectl._logger.getChild("config") 32 | config_section = "depthchargectl/config" 33 | 34 | @depthchargectl.board.copy() 35 | def board(self, codename=""): 36 | """Assume we're running on the specified board""" 37 | # We can query configs without knowing the board. 38 | try: 39 | return super().board 40 | except Exception as err: 41 | self.logger.warning(err) 42 | return None 43 | 44 | @Group 45 | def positionals(self): 46 | """Positional arguments""" 47 | 48 | @positionals.add 49 | @Argument 50 | def key(self, key): 51 | """Config key to get value of.""" 52 | return key 53 | 54 | @Group 55 | def options(self): 56 | """Options""" 57 | 58 | @options.add 59 | @Argument("--section", nargs=1) 60 | def section(self, section=None): 61 | """Config section to work on.""" 62 | parser = self.config.parser 63 | 64 | if section is None: 65 | section = self.config.name 66 | 67 | if section not in parser.sections(): 68 | if section != parser.default_section: 69 | parser.add_section(section) 70 | 71 | return parser[section] 72 | 73 | @options.add 74 | @Argument("--default", nargs=1) 75 | def default(self, default=None): 76 | """Value to return if key doesn't exist in section.""" 77 | return default 78 | 79 | def __call__(self): 80 | if self.key not in self.section: 81 | if self.default is not None: 82 | return self.default 83 | else: 84 | raise KeyError( 85 | "Key '{}' not found in section '{}'." 86 | .format(self.key, self.section.name) 87 | ) 88 | 89 | return self.section[self.key] 90 | 91 | global_options = depthchargectl.global_options 92 | config_options = depthchargectl.config_options 93 | -------------------------------------------------------------------------------- /depthcharge_tools/depthchargectl/_list.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | # depthcharge-tools depthchargectl list subcommand 5 | # Copyright (C) 2020-2022 Alper Nebi Yasak 6 | # See COPYRIGHT and LICENSE files for full copyright information. 7 | 8 | import argparse 9 | import logging 10 | import subprocess 11 | 12 | from depthcharge_tools import __version__ 13 | from depthcharge_tools.utils.argparse import ( 14 | Command, 15 | Argument, 16 | Group, 17 | CommandExit, 18 | ) 19 | from depthcharge_tools.utils.collections import ( 20 | TypedList 21 | ) 22 | from depthcharge_tools.utils.os import ( 23 | Disk, 24 | CrosPartition, 25 | ) 26 | 27 | from depthcharge_tools.depthchargectl import depthchargectl 28 | 29 | 30 | class CrosPartitions(TypedList(CrosPartition)): 31 | def __init__(self, partitions=None, columns=None, headings=True): 32 | super().__init__(partitions) 33 | 34 | if columns is None: 35 | if any(part.path is None for part in partitions): 36 | columns = ["S", "P", "T", "DISKPATH", "PARTNO"] 37 | else: 38 | columns = ["S", "P", "T", "PATH"] 39 | 40 | self._headings = headings 41 | self._columns = columns 42 | 43 | def _row(self, part): 44 | values = {} 45 | 46 | if set(self._columns).intersection(( 47 | "A", "S", "P", "T", 48 | "ATTRIBUTE", "SUCCESSFUL", "PRIORITY", "TRIES", 49 | )): 50 | flags = part.flags 51 | values.update({ 52 | "A": flags["attribute"], 53 | "S": flags["successful"], 54 | "P": flags["priority"], 55 | "T": flags["tries"], 56 | "ATTRIBUTE": flags["attribute"], 57 | "SUCCESSFUL": flags["successful"], 58 | "PRIORITY": flags["priority"], 59 | "TRIES": flags["tries"], 60 | }) 61 | 62 | if "SIZE" in self._columns: 63 | values["SIZE"] = part.size 64 | 65 | if part.path is not None: 66 | values["PATH"] = part.path 67 | 68 | if part.disk is not None and part.disk.path is not None: 69 | values["DISK"] = part.disk.path 70 | values["DISKPATH"] = part.disk.path 71 | 72 | if part.partno is not None: 73 | values["PARTNO"] = part.partno 74 | 75 | return [str(values.get(c, "")) for c in self._columns] 76 | 77 | def __str__(self): 78 | rows = [] 79 | 80 | if self._headings: 81 | rows.append(self._columns) 82 | 83 | parts = sorted(self, key=lambda p: p.path or p.disk.path) 84 | rows.extend(self._row(part) for part in parts) 85 | 86 | # Using tab characters makes things misalign when the data 87 | # widths vary, so find max width for each column from its data, 88 | # and format everything to those widths. 89 | widths = [max(4, *map(len, col)) for col in zip(*rows)] 90 | fmt = " ".join("{{:{w}}}".format(w=w) for w in widths) 91 | return "\n".join(fmt.format(*row) for row in rows) 92 | 93 | 94 | @depthchargectl.subcommand("list") 95 | class depthchargectl_list( 96 | depthchargectl, 97 | prog="depthchargectl list", 98 | usage="%(prog)s [options] [DISK ...]", 99 | add_help=False, 100 | ): 101 | """List ChromeOS kernel partitions.""" 102 | 103 | _logger = depthchargectl._logger.getChild("list") 104 | config_section = "depthchargectl/list" 105 | 106 | @depthchargectl.board.copy() 107 | def board(self, codename=""): 108 | """Assume we're running on the specified board""" 109 | # We can list partitions without knowing the board. 110 | try: 111 | return super().board 112 | except Exception as err: 113 | self.logger.warning(err) 114 | return None 115 | 116 | @Group 117 | def positionals(self): 118 | """Positional arguments""" 119 | 120 | @positionals.add 121 | @Argument(metavar="DISK") 122 | def disks(self, *disks): 123 | """Disks to check for ChromeOS kernel partitions.""" 124 | 125 | if self.all_disks: 126 | self.logger.info("Searching all disks.") 127 | disks = self.diskinfo.roots() 128 | elif disks: 129 | self.logger.info( 130 | "Searching real disks for {}." 131 | .format(", ".join(str(d) for d in disks)) 132 | ) 133 | images = [] 134 | for d in disks: 135 | if self.diskinfo.evaluate(d) is None: 136 | try: 137 | images.append(Disk(d)) 138 | except ValueError as err: 139 | self.logger.warning( 140 | err, 141 | exc_info=self.logger.isEnabledFor(logging.DEBUG), 142 | ) 143 | disks = [*self.diskinfo.roots(*disks), *images] 144 | else: 145 | self.logger.info("Searching bootable disks.") 146 | root = ( 147 | self.diskinfo.by_mountpoint("/", fstab_only=True) 148 | or self.diskinfo.by_mountpoint(self.root_mountpoint) 149 | ) 150 | boot = ( 151 | self.diskinfo.by_mountpoint("/boot", fstab_only=True) 152 | or self.diskinfo.by_mountpoint(self.boot_mountpoint) 153 | ) 154 | disks = self.diskinfo.roots(root, boot) 155 | 156 | if disks: 157 | self.logger.info( 158 | "Using disks: {}." 159 | .format(", ".join(str(d) for d in disks)) 160 | ) 161 | else: 162 | raise ValueError("Could not find any matching disks.") 163 | 164 | return disks 165 | 166 | @Group 167 | def options(self): 168 | """Options""" 169 | if self.count and self.output: 170 | raise ValueError( 171 | "Count and Output arguments are mutually exclusive." 172 | ) 173 | 174 | @options.add 175 | @Argument("-n", "--noheadings", headings=False) 176 | def headings(self, headings=True): 177 | """Don't print column headings.""" 178 | return headings 179 | 180 | @options.add 181 | @Argument("-a", "--all-disks", all_disks=True) 182 | def all_disks(self, all_disks=False): 183 | """List partitions on all disks.""" 184 | return all_disks 185 | 186 | valid_columns = { 187 | "ATTRIBUTE", "SUCCESSFUL", "PRIORITY", "TRIES", 188 | "A", "S", "P", "T", 189 | "PATH", 190 | "DISKPATH", "DISK", 191 | "PARTNO", 192 | "SIZE", 193 | } 194 | 195 | @options.add 196 | @Argument("-o", "--output", nargs=1, append=True) 197 | def output(self, *columns): 198 | """Comma separated list of columns to output.""" 199 | 200 | if len(columns) == 0: 201 | self.logger.info("Using default output format.") 202 | return None 203 | 204 | elif len(columns) == 1 and isinstance(columns[0], str): 205 | columns = columns[0] 206 | self.logger.info("Using output format '{}'.".format(columns)) 207 | 208 | else: 209 | columns = ",".join(columns) 210 | self.logger.info("Using output format '{}'.".format(columns)) 211 | 212 | columns = columns.split(',') 213 | 214 | invalid_columns = sorted( 215 | set(columns).difference(self.valid_columns), 216 | key=columns.index, 217 | ) 218 | if invalid_columns: 219 | raise ValueError( 220 | "Unsupported output columns '{}'." 221 | .format(invalid_columns) 222 | ) 223 | 224 | return columns 225 | 226 | @options.add 227 | @Argument("-c", "--count", count=True) 228 | def count(self, count=False): 229 | """Print only the count of partitions.""" 230 | return count 231 | 232 | def __call__(self): 233 | parts = [] 234 | error_disks = [] 235 | 236 | for disk in self.disks: 237 | try: 238 | parts.extend(disk.cros_partitions()) 239 | except subprocess.CalledProcessError as err: 240 | error_disks.append(disk) 241 | self.logger.debug( 242 | "Couldn't get partitions for disk '{}'." 243 | .format(disk) 244 | ) 245 | self.logger.debug( 246 | err, 247 | exc_info=self.logger.isEnabledFor(logging.DEBUG), 248 | ) 249 | 250 | if self.count: 251 | output = len(parts) 252 | 253 | else: 254 | output = CrosPartitions( 255 | parts, 256 | headings=self.headings, 257 | columns=self.output, 258 | ) 259 | 260 | if error_disks: 261 | return CommandExit( 262 | message=( 263 | "Couldn't get partitions for disks {}." 264 | .format(", ".join(str(d) for d in error_disks)) 265 | ), 266 | output=output, 267 | returncode=1, 268 | ) 269 | 270 | return output 271 | 272 | global_options = depthchargectl.global_options 273 | config_options = depthchargectl.config_options 274 | 275 | -------------------------------------------------------------------------------- /depthcharge_tools/depthchargectl/_remove.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | # depthcharge-tools depthchargectl remove subcommand 5 | # Copyright (C) 2020-2022 Alper Nebi Yasak 6 | # See COPYRIGHT and LICENSE files for full copyright information. 7 | 8 | import argparse 9 | import logging 10 | import subprocess 11 | 12 | from pathlib import Path 13 | 14 | from depthcharge_tools import ( 15 | __version__, 16 | ) 17 | from depthcharge_tools.utils.argparse import ( 18 | Command, 19 | Argument, 20 | Group, 21 | CommandExit, 22 | ) 23 | 24 | from depthcharge_tools.depthchargectl import depthchargectl 25 | 26 | 27 | class BootedPartitionError(CommandExit): 28 | def __init__(self, partition): 29 | self.partition = partition 30 | super().__init__( 31 | "Refusing to disable currently booted partition '{}'." 32 | .format(partition) 33 | ) 34 | 35 | 36 | @depthchargectl.subcommand("remove") 37 | class depthchargectl_remove( 38 | depthchargectl, 39 | prog="depthchargectl remove", 40 | usage="%(prog)s [options] (KERNEL_VERSION | IMAGE)", 41 | add_help=False, 42 | ): 43 | """Remove images and disable partitions containing them.""" 44 | 45 | _logger = depthchargectl._logger.getChild("remove") 46 | config_section = "depthchargectl/remove" 47 | 48 | @depthchargectl.board.copy() 49 | def board(self, codename=""): 50 | """Assume we're running on the specified board""" 51 | # We can disable partitions without knowing the board. 52 | try: 53 | return super().board 54 | except Exception as err: 55 | self.logger.warning(err) 56 | return None 57 | 58 | @Group 59 | def positionals(self): 60 | """Positional arguments""" 61 | 62 | if self.image is not None and self.kernel_version is not None: 63 | raise ValueError( 64 | "Image and kernel_version arguments are mutually exclusive." 65 | ) 66 | 67 | if self.image is not None: 68 | image = self.image 69 | else: 70 | image = self.kernel_version 71 | 72 | if isinstance(image, str): 73 | # This can be run after the kernel is uninstalled, where the 74 | # version would no longer be valid, so don't check for that. 75 | # Instead just check if we have it as an image. 76 | img = (self.images_dir / "{}.img".format(image)).resolve() 77 | if img.parent == self.images_dir and img.is_file(): 78 | self.logger.info( 79 | "Disabling partitions for kernel version '{}'." 80 | .format(image) 81 | ) 82 | self.image = img 83 | self.kernel_version = image 84 | 85 | else: 86 | self.image = Path(image).resolve() 87 | self.kernel_version = None 88 | self.logger.info( 89 | "Disabling partitions for depthcharge image '{}'." 90 | .format(image) 91 | ) 92 | 93 | if not self.image.is_file(): 94 | raise TypeError( 95 | "Image to remove '{}' is not a file." 96 | .format(self.image) 97 | ) 98 | 99 | @positionals.add 100 | @Argument(dest=argparse.SUPPRESS, nargs=0) 101 | def kernel_version(self, kernel_version): 102 | """Installed kernel version to disable.""" 103 | return kernel_version 104 | 105 | @positionals.add 106 | @Argument 107 | def image(self, image): 108 | """Depthcharge image to disable.""" 109 | return image 110 | 111 | @Group 112 | def options(self): 113 | """Options""" 114 | 115 | @options.add 116 | @Argument("-f", "--force", force=True) 117 | def force(self, force=False): 118 | """Allow disabling the currently booted partition.""" 119 | return force 120 | 121 | def __call__(self): 122 | image = self.image 123 | 124 | # When called with --vblockonly vbutil_kernel creates a file of 125 | # size 64KiB == 0x10000. 126 | image_vblock = image.read_bytes()[:0x10000] 127 | 128 | partitions = depthchargectl.list( 129 | root=self.root, 130 | root_mountpoint=self.root_mountpoint, 131 | boot_mountpoint=self.boot_mountpoint, 132 | config=self.config, 133 | board=self.board, 134 | tmpdir=self.tmpdir / "list", 135 | images_dir=self.images_dir, 136 | vboot_keyblock=self.vboot_keyblock, 137 | vboot_public_key=self.vboot_public_key, 138 | vboot_private_key=self.vboot_private_key, 139 | kernel_cmdline=self.kernel_cmdline, 140 | ignore_initramfs=self.ignore_initramfs, 141 | verbosity=self.verbosity, 142 | ) 143 | 144 | self.logger.info( 145 | "Searching for Chrome OS Kernel partitions containing '{}'." 146 | .format(image) 147 | ) 148 | badparts = [] 149 | error_disks = [] 150 | 151 | for part in partitions: 152 | self.logger.info("Checking partition '{}'.".format(part)) 153 | 154 | # It's OK to check only the vblock header, as that 155 | # contains signatures on the content and those will be 156 | # different if the content is different. 157 | with part.path.open("rb") as p: 158 | if p.read(0x10000) == image_vblock: 159 | try: 160 | if part.attribute: 161 | badparts.append(part) 162 | except subprocess.CalledProcessError as err: 163 | self.logger.warning( 164 | "Couldn't get attribute for partition '{}'." 165 | .format(part) 166 | ) 167 | self.logger.debug( 168 | err, 169 | exc_info=self.logger.isEnabledFor( 170 | logging.DEBUG, 171 | ), 172 | ) 173 | 174 | current = self.diskinfo.by_kern_guid() 175 | if current in badparts: 176 | if self.force: 177 | self.logger.warning( 178 | "Deactivating the currently booted partition '{}'. " 179 | "This might make your system unbootable." 180 | .format(current) 181 | ) 182 | else: 183 | raise BootedPartitionError(current) 184 | 185 | done_parts = [] 186 | error_parts = [] 187 | for part in badparts: 188 | self.logger.info("Deactivating '{}'.".format(part)) 189 | try: 190 | depthchargectl.bless( 191 | partition=part, 192 | bad=True, 193 | root=self.root, 194 | root_mountpoint=self.root_mountpoint, 195 | boot_mountpoint=self.boot_mountpoint, 196 | config=self.config, 197 | board=self.board, 198 | tmpdir=self.tmpdir / "bless", 199 | images_dir=self.images_dir, 200 | vboot_keyblock=self.vboot_keyblock, 201 | vboot_public_key=self.vboot_public_key, 202 | vboot_private_key=self.vboot_private_key, 203 | kernel_cmdline=self.kernel_cmdline, 204 | ignore_initramfs=self.ignore_initramfs, 205 | verbosity=self.verbosity, 206 | ) 207 | except Exception as err: 208 | error_parts.append(part) 209 | self.logger.debug( 210 | err, 211 | exc_info=self.logger.isEnabledFor(logging.DEBUG), 212 | ) 213 | continue 214 | 215 | done_parts.append(part) 216 | self.logger.warning("Deactivated '{}'.".format(part)) 217 | 218 | if image.parent == self.images_dir and not error_disks and not error_parts: 219 | self.logger.info( 220 | "Image '{}' is in images dir, deleting." 221 | .format(image) 222 | ) 223 | image.unlink() 224 | self.logger.warning("Deleted image '{}'.".format(image)) 225 | 226 | else: 227 | self.logger.info( 228 | "Not deleting image file '{}'." 229 | .format(image) 230 | ) 231 | 232 | output = badparts or None 233 | 234 | error_msg = [] 235 | if error_disks: 236 | error_msg.append( 237 | "Couldn't disable partitions for disks {}." 238 | .format(", ".join(str(d) for d in error_disks)) 239 | ) 240 | 241 | if error_parts: 242 | error_msg.append( 243 | "Couldn't disable partitions {}." 244 | .format(", ".join(str(d) for d in error_parts)) 245 | ) 246 | 247 | if error_msg: 248 | return CommandExit( 249 | message="\n".join(error_msg), 250 | output=done_parts, 251 | returncode=1, 252 | ) 253 | 254 | if not output: 255 | self.logger.warning( 256 | "No active partitions contain the given image." 257 | ) 258 | 259 | return output 260 | 261 | global_options = depthchargectl.global_options 262 | config_options = depthchargectl.config_options 263 | -------------------------------------------------------------------------------- /depthcharge_tools/depthchargectl/_target.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | # depthcharge-tools depthchargectl target subcommand 5 | # Copyright (C) 2020-2022 Alper Nebi Yasak 6 | # See COPYRIGHT and LICENSE files for full copyright information. 7 | 8 | import argparse 9 | import logging 10 | import subprocess 11 | import sys 12 | import types 13 | 14 | from depthcharge_tools import __version__ 15 | from depthcharge_tools.utils.argparse import ( 16 | Command, 17 | Argument, 18 | Group, 19 | CommandExit, 20 | ) 21 | from depthcharge_tools.utils.os import ( 22 | Disk, 23 | CrosPartition, 24 | Partition, 25 | ) 26 | from depthcharge_tools.utils.string import ( 27 | parse_bytesize, 28 | ) 29 | 30 | from depthcharge_tools.depthchargectl import depthchargectl 31 | 32 | 33 | class NotABlockDeviceError(CommandExit): 34 | def __init__(self, device): 35 | message = ( 36 | "Target '{}' is not a valid block device." 37 | .format(device) 38 | ) 39 | 40 | self.device = device 41 | super().__init__(message=message, returncode=2) 42 | 43 | 44 | class NotCrosPartitionError(CommandExit): 45 | def __init__(self, partition): 46 | message = ( 47 | "Partition '{}' is not of type Chrome OS Kernel." 48 | .format(partition) 49 | ) 50 | 51 | self.partition = partition 52 | super().__init__(message=message, returncode=5) 53 | 54 | 55 | class BootedPartitionError(CommandExit): 56 | def __init__(self, partition): 57 | message = ( 58 | "Partition '{}' is the currently booted parttiion." 59 | .format(partition) 60 | ) 61 | 62 | self.partition = partition 63 | super().__init__(message=message, returncode=6) 64 | 65 | 66 | class PartitionSizeTooSmallError(CommandExit): 67 | def __init__(self, partition, part_size, min_size): 68 | message = ( 69 | "Partition '{}' ('{}' bytes) is smaller than '{}' bytes." 70 | .format(partition, part_size, min_size) 71 | ) 72 | 73 | self.partition = partition 74 | self.part_size = part_size 75 | self.min_size = min_size 76 | super().__init__(message=message, returncode=7) 77 | 78 | 79 | class NoUsableCrosPartition(CommandExit): 80 | def __init__(self): 81 | message = ( 82 | "No usable Chrome OS Kernel partition found " 83 | "for given input arguments." 84 | ) 85 | 86 | super().__init__(message=message, output=None) 87 | 88 | 89 | @depthchargectl.subcommand("target") 90 | class depthchargectl_target( 91 | depthchargectl, 92 | prog="depthchargectl target", 93 | usage="%(prog)s [options] [PARTITION | DISK ...]", 94 | add_help=False, 95 | ): 96 | """Choose or validate a ChromeOS Kernel partition to use.""" 97 | 98 | _logger = depthchargectl._logger.getChild("target") 99 | config_section = "depthchargectl/target" 100 | 101 | @depthchargectl.board.copy() 102 | def board(self, codename=""): 103 | """Assume we're running on the specified board""" 104 | # We can target partitions without knowing the board. 105 | try: 106 | return super().board 107 | except Exception as err: 108 | self.logger.warning(err) 109 | return None 110 | 111 | @Group 112 | def positionals(self): 113 | """Positional arguments""" 114 | 115 | disks = list(self.disks) 116 | partitions = list(self.partitions) 117 | 118 | # The inputs can be a mixed list of partitions and disks, 119 | # separate the two. 120 | for d in list(disks): 121 | try: 122 | partitions.append(Partition(d)) 123 | self.logger.info("Using target '{}' as a partition.".format(d)) 124 | disks.remove(d) 125 | except: 126 | pass 127 | 128 | self.disks = disks 129 | self.partitions = partitions 130 | 131 | @positionals.add 132 | @Argument(metavar="PARTITION", nargs=0) 133 | def partitions(self, *partitions): 134 | """Chrome OS kernel partition to validate.""" 135 | return partitions 136 | 137 | @positionals.add 138 | @Argument(metavar="DISK") 139 | def disks(self, *disks): 140 | """Disks to search for an appropriate Chrome OS kernel partition.""" 141 | return disks 142 | 143 | @Group 144 | def options(self): 145 | """Options""" 146 | 147 | @options.add 148 | @Argument("-s", "--min-size", nargs=1) 149 | def min_size(self, bytes_=None): 150 | """Target partitions larger than this size.""" 151 | if bytes_ is None: 152 | return 0x10000 153 | 154 | return parse_bytesize(bytes_) 155 | 156 | @options.add 157 | @Argument("--allow-current", allow=True) 158 | def allow_current(self, allow=False): 159 | """Allow targeting the currently booted partition.""" 160 | return allow 161 | 162 | @options.add 163 | @Argument("-a", "--all-disks", all_disks=True) 164 | def all_disks(self, all_disks=False): 165 | """Target partitions on all disks.""" 166 | return all_disks 167 | 168 | def __call__(self): 169 | disks = list(self.disks) 170 | partitions = list(self.partitions) 171 | 172 | # We will need to check partitions against this if allow_current 173 | # is false. 174 | current = self.diskinfo.by_kern_guid() 175 | 176 | # Given a single partition, check if the partition is valid. 177 | if len(partitions) == 1 and len(disks) == 0: 178 | part = partitions[0] 179 | 180 | self.logger.info("Checking if target partition is writable.") 181 | if part.path is not None and not part.path.is_block_device(): 182 | raise NotABlockDeviceError(part.path) 183 | 184 | self.logger.info("Checking if targeted partition's disk is writable.") 185 | if not part.disk.path.is_block_device(): 186 | raise NotABlockDeviceError(part.disk.path) 187 | 188 | self.logger.info( 189 | "Checking if targeted partition's type is Chrome OS Kernel." 190 | ) 191 | if part not in part.disk.cros_partitions(): 192 | raise NotCrosPartitionError(part) 193 | 194 | self.logger.info( 195 | "Checking if targeted partition is currently booted one." 196 | ) 197 | if current is not None and not self.allow_current: 198 | if part.path == current.path: 199 | raise BootedPartitionError(part) 200 | 201 | self.logger.info( 202 | "Checking if targeted partition is bigger than given " 203 | "minimum size." 204 | ) 205 | if self.min_size is not None and part.size < self.min_size: 206 | raise PartitionSizeTooSmallError( 207 | part, 208 | part.size, 209 | self.min_size, 210 | ) 211 | 212 | # For arguments which are disks, search all their partitions. 213 | # If no disks or partitions were given, search bootable disks. 214 | # Search all disks if explicitly asked. 215 | if disks or not partitions or self.all_disks: 216 | partitions += depthchargectl.list( 217 | disks=disks, 218 | all_disks=self.all_disks, 219 | root=self.root, 220 | root_mountpoint=self.root_mountpoint, 221 | boot_mountpoint=self.boot_mountpoint, 222 | config=self.config, 223 | board=self.board, 224 | tmpdir=self.tmpdir / "list", 225 | images_dir=self.images_dir, 226 | vboot_keyblock=self.vboot_keyblock, 227 | vboot_public_key=self.vboot_public_key, 228 | vboot_private_key=self.vboot_private_key, 229 | kernel_cmdline=self.kernel_cmdline, 230 | ignore_initramfs=self.ignore_initramfs, 231 | verbosity=self.verbosity, 232 | ) 233 | 234 | good_partitions = [] 235 | for p in partitions: 236 | if self.min_size is not None and p.size < self.min_size: 237 | self.logger.warning( 238 | "Skipping partition '{}' as too small." 239 | .format(p) 240 | ) 241 | continue 242 | 243 | if current is not None and not self.allow_current: 244 | if p.path == current.path: 245 | self.logger.info( 246 | "Skipping currently booted partition '{}'." 247 | .format(p) 248 | ) 249 | continue 250 | 251 | self.logger.info("Partition '{}' is usable.".format(p)) 252 | good_partitions.append( 253 | CrosPartition(p.disk.path, partno=p.partno), 254 | ) 255 | 256 | # Get the least-successful, least-priority, least-tries-left 257 | # partition in that order of preference. 258 | if good_partitions: 259 | return min(good_partitions) 260 | else: 261 | return NoUsableCrosPartition() 262 | 263 | global_options = depthchargectl.global_options 264 | config_options = depthchargectl.config_options 265 | 266 | 267 | -------------------------------------------------------------------------------- /depthcharge_tools/depthchargectl/_write.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | # depthcharge-tools depthchargectl write subcommand 5 | # Copyright (C) 2020-2022 Alper Nebi Yasak 6 | # See COPYRIGHT and LICENSE files for full copyright information. 7 | 8 | import argparse 9 | import logging 10 | import os 11 | import subprocess 12 | 13 | from pathlib import Path 14 | 15 | from depthcharge_tools import ( 16 | __version__, 17 | ) 18 | from depthcharge_tools.utils.argparse import ( 19 | Command, 20 | Argument, 21 | Group, 22 | CommandExit, 23 | ) 24 | from depthcharge_tools.utils.platform import ( 25 | KernelEntry, 26 | installed_kernels, 27 | ) 28 | 29 | from depthcharge_tools.depthchargectl import depthchargectl 30 | 31 | 32 | class ImageBuildError(CommandExit): 33 | def __init__(self, kernel_version=None): 34 | self.kernel_version = kernel_version 35 | 36 | if kernel_version is None: 37 | message = "Failed to build depthcharge image." 38 | 39 | else: 40 | message = ( 41 | "Failed to build depthcharge image for kernel version '{}'." 42 | .format(kernel_version) 43 | ) 44 | 45 | super().__init__(message=message) 46 | 47 | 48 | class NotBootableImageError(CommandExit): 49 | def __init__(self, image): 50 | self.image = image 51 | super().__init__( 52 | "Image '{}' is not bootable on this board." 53 | .format(image) 54 | ) 55 | 56 | 57 | class NoUsableCrosPartitionError(CommandExit): 58 | def __init__(self): 59 | super().__init__( 60 | "No usable Chrome OS Kernel partition found." 61 | ) 62 | 63 | 64 | @depthchargectl.subcommand("write") 65 | class depthchargectl_write( 66 | depthchargectl, 67 | prog="depthchargectl write", 68 | usage="%(prog)s [options] [KERNEL-VERSION | IMAGE]", 69 | add_help=False, 70 | ): 71 | """Write an image to a ChromeOS kernel partition.""" 72 | 73 | _logger = depthchargectl._logger.getChild("write") 74 | config_section = "depthchargectl/write" 75 | 76 | @depthchargectl.board.copy() 77 | def board(self, codename=""): 78 | """Assume we're running on the specified board""" 79 | # We can write images to partitions without knowing the board. 80 | # The image argument will become required if this returns None. 81 | try: 82 | return super().board 83 | except Exception as err: 84 | self.logger.warning(err) 85 | return None 86 | 87 | @Group 88 | def positionals(self): 89 | """Positional arguments""" 90 | 91 | if self.image is not None and self.kernel_version is not None: 92 | raise ValueError( 93 | "Image and kernel_version arguments are mutually exclusive" 94 | ) 95 | 96 | arg = self.image or self.kernel_version 97 | 98 | # Turn arg into a relevant KernelEntry if it's a kernel version 99 | # or a Path() if not 100 | if isinstance(arg, str): 101 | arg = max( 102 | (k for k in installed_kernels() if k.release == arg), 103 | default=Path(arg).resolve(), 104 | ) 105 | 106 | if isinstance(arg, KernelEntry): 107 | self.image = None 108 | self.kernel_version = arg 109 | 110 | elif isinstance(arg, Path): 111 | self.image = arg 112 | self.kernel_version = None 113 | 114 | if self.board is None and self.image is None: 115 | raise ValueError( 116 | "An image file is required when no board is specified." 117 | ) 118 | 119 | @positionals.add 120 | @Argument(dest=argparse.SUPPRESS, nargs=0) 121 | def kernel_version(self, kernel_version): 122 | """Installed kernel version to write to disk.""" 123 | return kernel_version 124 | 125 | @positionals.add 126 | @Argument 127 | def image(self, image=None): 128 | """Depthcharge image to write to disk.""" 129 | return image 130 | 131 | @Group 132 | def options(self): 133 | """Options""" 134 | 135 | @options.add 136 | @Argument("-f", "--force", force=True) 137 | def force(self, force=False): 138 | """Write image even if it cannot be verified.""" 139 | return force 140 | 141 | @options.add 142 | @Argument("-t", "--target", metavar="DISK|PART") 143 | def target(self, target): 144 | """Specify a disk or partition to write to.""" 145 | return target 146 | 147 | @options.add 148 | @Argument("--no-prioritize", prioritize=False) 149 | def prioritize(self, prioritize=True): 150 | """Don't set any flags on the partition.""" 151 | return prioritize 152 | 153 | @options.add 154 | @Argument("--allow-current", allow=True) 155 | def allow_current(self, allow=False): 156 | """Allow overwriting the currently booted partition.""" 157 | return allow 158 | 159 | def __call__(self): 160 | if self.board is None: 161 | self.logger.warning( 162 | "Using given image '{}' without board-specific checks." 163 | .format(self.image) 164 | ) 165 | image = self.image 166 | 167 | elif self.image is not None: 168 | self.logger.info("Using given image '{}'." .format(self.image)) 169 | image = self.image 170 | 171 | try: 172 | depthchargectl.check( 173 | image=image, 174 | config=self.config, 175 | board=self.board, 176 | tmpdir=self.tmpdir / "check", 177 | images_dir=self.images_dir, 178 | vboot_keyblock=self.vboot_keyblock, 179 | vboot_public_key=self.vboot_public_key, 180 | vboot_private_key=self.vboot_private_key, 181 | kernel_cmdline=self.kernel_cmdline, 182 | ignore_initramfs=self.ignore_initramfs, 183 | verbosity=self.verbosity, 184 | ) 185 | 186 | except Exception as err: 187 | if self.force: 188 | self.logger.warning( 189 | "Image '{}' is not bootable on this board, " 190 | "continuing due to --force." 191 | .format(image) 192 | ) 193 | 194 | else: 195 | raise NotBootableImageError(image) from err 196 | 197 | else: 198 | # No image given, try creating one. 199 | try: 200 | image = depthchargectl.build_( 201 | kernel_version=self.kernel_version, 202 | root=self.root, 203 | root_mountpoint=self.root_mountpoint, 204 | boot_mountpoint=self.boot_mountpoint, 205 | config=self.config, 206 | board=self.board, 207 | tmpdir=self.tmpdir / "build", 208 | images_dir=self.images_dir, 209 | vboot_keyblock=self.vboot_keyblock, 210 | vboot_public_key=self.vboot_public_key, 211 | vboot_private_key=self.vboot_private_key, 212 | kernel_cmdline=self.kernel_cmdline, 213 | ignore_initramfs=self.ignore_initramfs, 214 | verbosity=self.verbosity, 215 | ) 216 | 217 | except Exception as err: 218 | raise ImageBuildError(self.kernel_version) from err 219 | 220 | # We don't want target to unconditionally avoid the current 221 | # partition since we will also check that here. But whatever we 222 | # choose must be bigger than the image we'll write to it. 223 | self.logger.info("Searching disks for a target partition.") 224 | try: 225 | target = depthchargectl.target( 226 | disks=[self.target] if self.target else [], 227 | min_size=image.stat().st_size, 228 | allow_current=self.allow_current, 229 | root=self.root, 230 | root_mountpoint=self.root_mountpoint, 231 | boot_mountpoint=self.boot_mountpoint, 232 | config=self.config, 233 | board=self.board, 234 | tmpdir=self.tmpdir / "target", 235 | images_dir=self.images_dir, 236 | vboot_keyblock=self.vboot_keyblock, 237 | vboot_public_key=self.vboot_public_key, 238 | vboot_private_key=self.vboot_private_key, 239 | kernel_cmdline=self.kernel_cmdline, 240 | ignore_initramfs=self.ignore_initramfs, 241 | verbosity=self.verbosity, 242 | ) 243 | 244 | except Exception as err: 245 | raise NoUsableCrosPartitionError() from err 246 | 247 | if target is None: 248 | raise NoUsableCrosPartitionError() 249 | 250 | self.logger.info("Targeted partition '{}'.".format(target)) 251 | 252 | # Check and warn if we targeted the currently booted partition, 253 | # as that usually means it's the only partition. 254 | current = self.diskinfo.by_kern_guid() 255 | if current is not None and self.allow_current and target.path == current.path: 256 | self.logger.warning( 257 | "Overwriting the currently booted partition '{}'. " 258 | "This might make your system unbootable." 259 | .format(target) 260 | ) 261 | 262 | self.logger.info( 263 | "Writing image '{}' to partition '{}'." 264 | .format(image, target) 265 | ) 266 | target.write_bytes(image.read_bytes()) 267 | self.logger.warning( 268 | "Wrote image '{}' to partition '{}'." 269 | .format(image, target) 270 | ) 271 | 272 | if self.prioritize: 273 | self.logger.info( 274 | "Setting '{}' as the highest-priority bootable part." 275 | .format(target) 276 | ) 277 | try: 278 | depthchargectl.bless( 279 | partition=target, 280 | oneshot=True, 281 | root=self.root, 282 | root_mountpoint=self.root_mountpoint, 283 | boot_mountpoint=self.boot_mountpoint, 284 | config=self.config, 285 | board=self.board, 286 | tmpdir=self.tmpdir / "bless", 287 | images_dir=self.images_dir, 288 | vboot_keyblock=self.vboot_keyblock, 289 | vboot_public_key=self.vboot_public_key, 290 | vboot_private_key=self.vboot_private_key, 291 | kernel_cmdline=self.kernel_cmdline, 292 | ignore_initramfs=self.ignore_initramfs, 293 | verbosity=self.verbosity, 294 | ) 295 | except Exception as err: 296 | raise CommandExit( 297 | "Failed to set '{}' as the highest-priority bootable part." 298 | .format(target) 299 | ) from err 300 | 301 | self.logger.warning( 302 | "Set partition '{}' as next to boot." 303 | .format(target) 304 | ) 305 | 306 | return target 307 | 308 | global_options = depthchargectl.global_options 309 | config_options = depthchargectl.config_options 310 | 311 | -------------------------------------------------------------------------------- /depthcharge_tools/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alpernebbi/depthcharge-tools/95d17c444ae3a9dec1c51ead6301ed04777314b9/depthcharge_tools/utils/__init__.py -------------------------------------------------------------------------------- /depthcharge_tools/utils/collections.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | # depthcharge-tools collections utilities 5 | # Copyright (C) 2021-2022 Alper Nebi Yasak 6 | # See COPYRIGHT and LICENSE files for full copyright information. 7 | 8 | import collections 9 | 10 | # Inheritance for config sections 11 | class ConfigDict(collections.OrderedDict): 12 | def __getitem__(self, key): 13 | super_ = super() 14 | if not isinstance(key, str) or "/" not in key: 15 | return super_.__getitem__(key) 16 | 17 | def getitem(key): 18 | try: 19 | return super_.__getitem__(key) 20 | except KeyError: 21 | return KeyError 22 | 23 | def parents(leaf): 24 | idx = leaf.find("/") 25 | while idx != -1: 26 | yield leaf[:idx] 27 | idx = leaf.find("/", idx + 1) 28 | yield leaf 29 | 30 | items = list( 31 | item for item in reversed([ 32 | getitem(p) for p in parents(key) 33 | ]) if item != KeyError 34 | ) 35 | 36 | if all(isinstance(i, dict) for i in items): 37 | return collections.ChainMap(*items) 38 | 39 | if items: 40 | return items[0] 41 | 42 | raise KeyError(key) 43 | 44 | 45 | # To write config sections in sort order 46 | def SortedDict(key=None): 47 | if not callable(key): 48 | raise TypeError( 49 | "SortedDict argument must be a callable, not {}" 50 | .format(type(key).__name__) 51 | ) 52 | 53 | class SortedDict(collections.UserDict): 54 | __key = key 55 | 56 | def __iter__(self): 57 | yield from sorted(super().__iter__(), key=type(self).__key) 58 | 59 | return SortedDict 60 | 61 | 62 | def TypedList(T): 63 | if not isinstance(T, type): 64 | raise TypeError( 65 | "TypedList argument must be a type, not {}" 66 | .format(type(T).__name__) 67 | ) 68 | 69 | name = "TypedList.{}List".format(str.title(T.__name__)) 70 | 71 | class TypedList(collections.UserList): 72 | __type = T 73 | __name__ = name 74 | __qualname__ = name 75 | 76 | def __init__(self, initlist=None): 77 | if initlist is not None: 78 | self.__typecheck(*initlist) 79 | super().__init__(initlist) 80 | 81 | def __typecheck(self, *values): 82 | if not all(isinstance(value, self.__type) for value in values): 83 | raise TypeError( 84 | "{} items must be of type {}." 85 | .format(type(self).__name__, self.__type.__name__) 86 | ) 87 | 88 | def __setitem__(self, idx, value): 89 | self.__typecheck(value) 90 | return super().__setitem__(idx, value) 91 | 92 | def __iadd__(self, other): 93 | self.__typecheck(*other) 94 | return super().__iadd__(other) 95 | 96 | def append(self, value): 97 | self.__typecheck(value) 98 | return super().append(value) 99 | 100 | def insert(self, idx, value): 101 | self.__typecheck(value) 102 | return super().insert(idx, value) 103 | 104 | def extend(self, other): 105 | self.__typecheck(*other) 106 | return super().extend(other) 107 | 108 | return TypedList 109 | 110 | 111 | class DirectedGraph: 112 | def __init__(self): 113 | self.__edges = {} 114 | 115 | def add_edge(self, node, child): 116 | self.add_node(node) 117 | self.add_node(child) 118 | self.__edges[node].add(child) 119 | 120 | def add_node(self, node): 121 | if node not in self.__edges: 122 | self.__edges[node] = set() 123 | 124 | def remove_edge(self, node, child): 125 | if node in self.__edges: 126 | self.__edges[node].discard(child) 127 | 128 | def remove_node(self, node): 129 | self.__edges.pop(node, None) 130 | for k, v in self.__edges.items(): 131 | v.discard(node) 132 | 133 | def replace_node(self, node, replacement, merge=False): 134 | if replacement in self.__edges and not merge: 135 | raise ValueError( 136 | "Replacement node '{}' already in graph." 137 | .format(replacement) 138 | ) 139 | 140 | parents = self.parents(node) 141 | children = self.children(node) 142 | self.remove_node(node) 143 | 144 | self.add_node(replacement) 145 | for p in parents: 146 | self.add_edge(p, replacement) 147 | for c in children: 148 | self.add_edge(replacement, c) 149 | 150 | def edges(self): 151 | return set( 152 | (n, c) 153 | for n, cs in self.__edges.items() 154 | for c in cs 155 | ) 156 | 157 | def nodes(self): 158 | return set(self.__edges.keys()) 159 | 160 | def children(self, *nodes): 161 | node_children = set() 162 | for node in nodes: 163 | node_children.update(self.__edges.get(node, set())) 164 | 165 | return node_children 166 | 167 | def parents(self, *nodes): 168 | node_parents = set() 169 | for parent, children in self.__edges.items(): 170 | if children.intersection(nodes): 171 | node_parents.add(parent) 172 | 173 | return node_parents 174 | 175 | def ancestors(self, *nodes): 176 | nodes = set(nodes) 177 | 178 | ancestors = self.parents(*nodes) 179 | tmp = self.parents(*ancestors) 180 | while tmp - ancestors: 181 | ancestors.update(tmp) 182 | tmp = self.parents(*ancestors) 183 | 184 | return ancestors 185 | 186 | def descendants(self, *nodes): 187 | nodes = set(nodes) 188 | 189 | descendants = self.children(*nodes) 190 | tmp = self.children(*descendants) 191 | while tmp - descendants: 192 | descendants.update(tmp) 193 | tmp = self.children(*descendants) 194 | 195 | return descendants 196 | 197 | def leaves(self, *nodes): 198 | nodes = set(nodes) 199 | 200 | leaves = set() 201 | if len(nodes) == 0: 202 | leaves.update(k for k, v in self.__edges.items() if not v) 203 | return leaves 204 | 205 | leaves = self.leaves() 206 | node_leaves = set() 207 | while nodes: 208 | node_leaves.update(nodes.intersection(leaves)) 209 | nodes.difference_update(node_leaves) 210 | nodes = self.children(*nodes) 211 | 212 | return node_leaves 213 | 214 | def roots(self, *nodes): 215 | nodes = set(nodes) 216 | 217 | roots = set() 218 | if len(nodes) == 0: 219 | roots.update(self.__edges.keys()) 220 | roots.difference_update(*self.__edges.values()) 221 | return roots 222 | 223 | roots = self.roots() 224 | node_roots = set() 225 | while nodes: 226 | node_roots.update(nodes.intersection(roots)) 227 | nodes.difference_update(node_roots) 228 | nodes = self.parents(*nodes) 229 | 230 | return node_roots 231 | -------------------------------------------------------------------------------- /depthcharge_tools/utils/os.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | # depthcharge-tools os utilities 5 | # Copyright (C) 2020-2022 Alper Nebi Yasak 6 | # See COPYRIGHT and LICENSE files for full copyright information. 7 | 8 | import collections 9 | import re 10 | import shlex 11 | 12 | from pathlib import Path 13 | 14 | from depthcharge_tools.utils.collections import ( 15 | DirectedGraph, 16 | ) 17 | from depthcharge_tools.utils.pathlib import ( 18 | iterdir, 19 | read_lines, 20 | ) 21 | from depthcharge_tools.utils.platform import ( 22 | proc_cmdline, 23 | ) 24 | from depthcharge_tools.utils.subprocess import ( 25 | cgpt, 26 | ) 27 | 28 | 29 | class Disks(DirectedGraph): 30 | def __init__( 31 | self, 32 | sys="/sys", 33 | dev="/dev", 34 | fstab="/etc/fstab", 35 | mtab="/etc/mtab", 36 | procmounts="/proc/self/mounts", 37 | mountinfo="/proc/self/mountinfo", 38 | crypttab="/etc/crypttab", 39 | ): 40 | super().__init__() 41 | 42 | self._sys = sys = Path(sys) 43 | self._dev = dev = Path(dev) 44 | self._fstab = fstab = Path(fstab) 45 | self._procmounts = procmounts = Path(procmounts) 46 | self._mtab = mtab = Path(mtab) 47 | self._mountinfo = mountinfo = Path(mountinfo) 48 | self._crypttab = crypttab = Path(crypttab) 49 | 50 | for sysdir in iterdir(sys / "class" / "block"): 51 | for device in read_lines(sysdir / "dm" / "name"): 52 | self.add_edge(dev / sysdir.name, dev / "mapper" / device) 53 | 54 | for device in iterdir(sysdir / "slaves"): 55 | self.add_edge(dev / device.name, dev / sysdir.name) 56 | 57 | for device in iterdir(sysdir / "holders"): 58 | self.add_edge(dev / sysdir.name, dev / device.name) 59 | 60 | for device in iterdir(sysdir): 61 | if device.name.startswith(sysdir.name): 62 | self.add_edge(dev / sysdir.name, dev / device.name) 63 | 64 | for line in read_lines(crypttab): 65 | if line and not line.startswith("#"): 66 | fields = shlex.split(line) 67 | cryptdev, device = fields[0], fields[1] 68 | if device != 'none': 69 | cryptdev = dev / "mapper" / cryptdev 70 | self.add_edge(device, cryptdev) 71 | 72 | fstab_mounts = {} 73 | for line in read_lines(fstab): 74 | if line and not line.startswith("#"): 75 | fields = shlex.split(line) 76 | device, mount = fields[0], fields[1] 77 | if mount != 'none': 78 | fstab_mounts[mount] = device 79 | 80 | procmounts_mounts = {} 81 | for line in read_lines(procmounts): 82 | if line and not line.startswith("#"): 83 | fields = shlex.split(line) 84 | device, mount = fields[0], fields[1] 85 | device = self.evaluate(device) 86 | if device is not None: 87 | procmounts_mounts[mount] = device 88 | 89 | mtab_mounts = {} 90 | for line in read_lines(mtab): 91 | if line and not line.startswith("#"): 92 | fields = shlex.split(line) 93 | device, mount = fields[0], fields[1] 94 | device = self.evaluate(device) 95 | if device is not None: 96 | mtab_mounts[mount] = device 97 | 98 | mountinfo_mounts = {} 99 | for line in read_lines(mountinfo): 100 | if line and not line.startswith("#"): 101 | fields = shlex.split(line) 102 | device, fsroot, mount = fields[2], fields[3], fields[4] 103 | if fsroot != "/": 104 | mountinfo_mounts[mount] = None 105 | continue 106 | device = self.evaluate(device) 107 | if device is not None: 108 | mountinfo_mounts[mount] = device 109 | 110 | mounts = collections.ChainMap( 111 | fstab_mounts, 112 | mountinfo_mounts, 113 | procmounts_mounts, 114 | mtab_mounts, 115 | ) 116 | 117 | self._fstab_mounts = fstab_mounts 118 | self._procmounts_mounts = procmounts_mounts 119 | self._mtab_mounts = mtab_mounts 120 | self._mountinfo_mounts = mountinfo_mounts 121 | self._mounts = mounts 122 | 123 | def __getitem__(self, key): 124 | return self.evaluate(key) 125 | 126 | def evaluate(self, device): 127 | dev = self._dev 128 | sys = self._sys 129 | 130 | if device is None: 131 | return None 132 | 133 | elif isinstance(device, Path): 134 | device = str(device) 135 | 136 | elif isinstance(device, (Disk, Partition)): 137 | device = str(device.path) 138 | 139 | if device.startswith("ID="): 140 | id_ = device[len("ID="):] 141 | if not id_: 142 | return None 143 | 144 | device = dev / "disk" / "by-id" / id_ 145 | 146 | elif device.startswith("LABEL="): 147 | label = device[len("LABEL="):] 148 | if not label: 149 | return None 150 | 151 | device = dev / "disk" / "by-label" / label 152 | 153 | elif device.startswith("PARTLABEL="): 154 | partlabel = device[len("PARTLABEL="):] 155 | if not partlabel: 156 | return None 157 | 158 | device = dev / "disk" / "by-partlabel" / partlabel 159 | 160 | elif device.startswith("UUID="): 161 | uuid = device[len("UUID="):] 162 | if not uuid: 163 | return None 164 | 165 | device = dev / "disk" / "by-uuid" / uuid 166 | if not device.exists(): 167 | device = dev / "disk" / "by-uuid" / uuid.lower() 168 | 169 | elif device.startswith("PARTUUID="): 170 | partuuid, _, partnroff = ( 171 | device[len("PARTUUID="):].partition("/PARTNROFF=") 172 | ) 173 | if not partuuid: 174 | return None 175 | 176 | device = dev / "disk" / "by-partuuid" / partuuid 177 | if not device.exists(): 178 | device = dev / "disk" / "by-partuuid" / partuuid.lower() 179 | 180 | if partnroff: 181 | device = device.resolve() 182 | match = re.match("(.*[^0-9])([0-9]+)$", device.name) 183 | if not match: 184 | return None 185 | prefix, partno = match.groups() 186 | partno = str(int(partno) + int(partnroff)) 187 | device = device.with_name("{}{}".format(prefix, partno)) 188 | 189 | elif re.match("[0-9]+:[0-9]+", device): 190 | device = dev / "block" / device 191 | 192 | # Encrypted devices may currently be set up with names different 193 | # than in the crypttab file, so check that as well. 194 | elif device.startswith(str(dev / "mapper")): 195 | if not Path(device).resolve().exists(): 196 | for line in read_lines(self._crypttab): 197 | if not line or line.startswith("#"): 198 | continue 199 | 200 | fields = shlex.split(line) 201 | parentdev, cryptdev = fields[1], fields[0] 202 | if cryptdev != device.split("/")[-1]: 203 | continue 204 | 205 | parentdev = self.evaluate(parentdev) 206 | siblings = self.children(parentdev) 207 | if len(siblings) == 1: 208 | device = str(siblings.pop()) 209 | 210 | # This is actually wrong, but we can't really decide 211 | # which to use. The parent's good enough for us since 212 | # we usually only care about going up the tree. 213 | else: 214 | device = str(parentdev) 215 | 216 | device = Path(device).resolve() 217 | if not device.exists() or dev not in device.parents: 218 | return None 219 | 220 | try: 221 | return Partition(device, dev=dev, sys=sys) 222 | except: 223 | pass 224 | 225 | try: 226 | return Disk(device, dev=dev, sys=sys) 227 | except: 228 | pass 229 | 230 | def by_mountpoint(self, mountpoint, fstab_only=False): 231 | if not Path(mountpoint).exists(): 232 | return None 233 | 234 | if fstab_only: 235 | # We want the form in the fstab, e.g. PARTUUID=* 236 | device = self._fstab_mounts.get(mountpoint) 237 | return device 238 | else: 239 | device = self._mounts.get(str(mountpoint)) 240 | return self.evaluate(device) 241 | 242 | def mountpoints(self, device, include_fstab=False): 243 | device = self.evaluate(device) 244 | if device is None: 245 | return set() 246 | 247 | # Exclude fstab whose entries are not necessarily mounted 248 | if not include_fstab: 249 | mounts = collections.ChainMap( 250 | self._mountinfo_mounts, 251 | self._procmounts_mounts, 252 | self._mtab_mounts, 253 | ) 254 | else: 255 | mounts = self._mounts 256 | 257 | mountpoints = set() 258 | for mnt, dev in mounts.items(): 259 | dev = self.evaluate(dev) 260 | if dev == device: 261 | mnt = Path(mnt).resolve() 262 | if mnt.exists() or include_fstab: 263 | mountpoints.add(mnt) 264 | 265 | return mountpoints 266 | 267 | def by_id(self, id_): 268 | return self.evaluate("ID={}".format(id_)) 269 | 270 | def by_label(self, label): 271 | return self.evaluate("LABEL={}".format(label)) 272 | 273 | def by_partlabel(self, partlabel): 274 | return self.evaluate("PARTLABEL={}".format(partlabel)) 275 | 276 | def by_uuid(self, uuid): 277 | return self.evaluate("UUID={}".format(uuid)) 278 | 279 | def by_partuuid(self, partuuid): 280 | return self.evaluate("PARTUUID={}".format(partuuid)) 281 | 282 | def _get_dev_disk_info(self, device, prop): 283 | device = self.evaluate(device) 284 | for path in self._dev.glob("disk/by-{}/*".format(prop)): 285 | dev = self.evaluate(path) 286 | if dev == device: 287 | return path.name 288 | 289 | def get_id(self, device): 290 | return self._get_dev_disk_info(device, "id") 291 | 292 | def get_label(self, device): 293 | return self._get_dev_disk_info(device, "label") 294 | 295 | def get_partlabel(self, device): 296 | return self._get_dev_disk_info(device, "partlabel") 297 | 298 | def get_uuid(self, device): 299 | return self._get_dev_disk_info(device, "uuid") 300 | 301 | def get_partuuid(self, device): 302 | return self._get_dev_disk_info(device, "partuuid") 303 | 304 | def by_kern_guid(self): 305 | for arg in proc_cmdline(): 306 | lhs, _, rhs = arg.partition("=") 307 | if lhs == "kern_guid": 308 | return self.by_partuuid(rhs) 309 | 310 | def add_edge(self, node, child): 311 | node = self.evaluate(node) 312 | child = self.evaluate(child) 313 | if node is not None and child is not None and node != child: 314 | return super().add_edge(node, child) 315 | 316 | def children(self, *nodes): 317 | return super().children(*map(self.evaluate, nodes)) 318 | 319 | def parents(self, *nodes): 320 | return super().parents(*map(self.evaluate, nodes)) 321 | 322 | def leaves(self, *nodes): 323 | return super().leaves(*map(self.evaluate, nodes)) 324 | 325 | def roots(self, *nodes): 326 | return super().roots(*map(self.evaluate, nodes)) 327 | 328 | 329 | class Disk: 330 | def __init__(self, path, dev="/dev", sys="/sys"): 331 | self._sys = sys = Path(sys) 332 | self._dev = dev = Path(dev) 333 | 334 | if isinstance(path, Disk): 335 | path = path.path 336 | else: 337 | path = Path(path).resolve() 338 | 339 | if not (path.is_file() or path.is_block_device()): 340 | fmt = "Disk '{}' is not a file or block device." 341 | msg = fmt.format(str(path)) 342 | raise ValueError(msg) 343 | 344 | self.path = path 345 | 346 | def partition(self, partno): 347 | return Partition(self, partno, dev=self._dev, sys=self._sys) 348 | 349 | def partitions(self): 350 | return [ 351 | Partition(self, n, dev=self._dev, sys=self._sys) 352 | for n in cgpt.find_partitions(self.path) 353 | ] 354 | 355 | def cros_partitions(self): 356 | return [ 357 | CrosPartition(self, n, dev=self._dev, sys=self._sys) 358 | for n in cgpt.find_partitions(self.path, type="kernel") 359 | ] 360 | 361 | @property 362 | def size(self): 363 | if self.path.is_file(): 364 | return self.path.stat().st_size 365 | 366 | if self.path.is_block_device(): 367 | sysdir = self._sys / "class" / "block" / self.path.name 368 | 369 | size_f = sysdir / "size" 370 | if size_f.exists(): 371 | blocks = int(size_f.read_text()) 372 | return blocks * 512 373 | 374 | def __hash__(self): 375 | return hash((self.path,)) 376 | 377 | def __eq__(self, other): 378 | if isinstance(other, Disk): 379 | return self.path == other.path 380 | return False 381 | 382 | def __str__(self): 383 | return str(self.path) 384 | 385 | def __repr__(self): 386 | cls = self.__class__.__name__ 387 | return "{}('{}')".format(cls, self.path) 388 | 389 | 390 | class Partition: 391 | def __init__(self, path, partno=None, dev="/dev", sys="/sys"): 392 | self._dev = dev = Path(dev) 393 | self._sys = sys = Path(sys) 394 | 395 | if isinstance(path, Disk): 396 | disk = path 397 | path = None 398 | elif isinstance(path, Partition): 399 | disk = path.disk 400 | partno = path.partno 401 | path = path.path 402 | else: 403 | disk = None 404 | path = Path(path).resolve() 405 | 406 | if ( 407 | disk is None 408 | and partno is None 409 | and path.parent == dev 410 | and path.is_block_device() 411 | ): 412 | match = ( 413 | re.fullmatch("(.*[0-9])p([0-9]+)", path.name) 414 | or re.fullmatch("(.*[^0-9])([0-9]+)", path.name) 415 | ) 416 | if match: 417 | diskname, partno = match.groups() 418 | partno = int(partno) 419 | disk = Disk(path.with_name(diskname), dev=dev, sys=sys) 420 | 421 | if disk is None: 422 | disk = Disk(path, dev=dev, sys=sys) 423 | path = None 424 | 425 | if partno is None: 426 | fmt = "Partition number not given for disk '{}'." 427 | msg = fmt.format(str(disk)) 428 | raise ValueError(msg) 429 | 430 | elif not (isinstance(partno, int) and partno > 0): 431 | fmt = "Partition number '{}' must be a positive integer." 432 | msg = fmt.format(partno) 433 | raise ValueError(msg) 434 | 435 | elif ( 436 | path is None 437 | and disk.path.parent == dev 438 | and disk.path.is_block_device() 439 | ): 440 | fmt = "{}p{}" if disk.path.name[-1].isnumeric() else "{}{}" 441 | name = fmt.format(disk.path.name, partno) 442 | path = disk.path.with_name(name) 443 | 444 | if path is not None: 445 | if not (path.is_file() or path.is_block_device()): 446 | path = None 447 | 448 | self.disk = disk 449 | self.path = path 450 | self.partno = partno 451 | 452 | @property 453 | def size(self): 454 | if self.path is None: 455 | return cgpt.get_size(self.disk.path, self.partno) 456 | 457 | if self.path.is_file(): 458 | return self.path.stat().st_size 459 | 460 | if self.path.is_block_device(): 461 | sysdir = self._sys / "class" / "block" / self.path.name 462 | 463 | size_f = sysdir / "size" 464 | if size_f.exists(): 465 | blocks = int(size_f.read_text()) 466 | return blocks * 512 467 | 468 | def write_bytes(self, data): 469 | data = bytes(data) 470 | 471 | if len(data) >= self.size: 472 | raise ValueError( 473 | "Data to be written ('{}' bytes) is bigger than " 474 | "partition '{}' ('{}' bytes)." 475 | .format(len(data), self, self.size) 476 | ) 477 | 478 | if self.path is None: 479 | start = cgpt.get_start(self.disk.path, self.partno) 480 | 481 | with self.disk.path.open("r+b") as disk: 482 | seek = disk.seek(start) 483 | if seek != start: 484 | raise IOError( 485 | "Couldn't seek disk to start of partition '{}'." 486 | .format(self) 487 | ) 488 | 489 | written = disk.write(data) 490 | if written != len(data): 491 | raise IOError( 492 | "Couldn't write data to partition '{}' " 493 | "(wrote '{}' out of '{}' bytes)." 494 | .format(self, written, len(data)) 495 | ) 496 | 497 | else: 498 | self.path.write_bytes(data) 499 | 500 | def __hash__(self): 501 | return hash((self.path, self.disk, self.partno)) 502 | 503 | def __eq__(self, other): 504 | if isinstance(other, Partition): 505 | return ( 506 | self.path == other.path 507 | and self.disk == other.disk 508 | and self.partno == other.partno 509 | ) 510 | return False 511 | 512 | def __str__(self): 513 | if self.path is not None: 514 | return str(self.path) 515 | else: 516 | return "{}#{}".format(self.disk.path, self.partno) 517 | 518 | def __repr__(self): 519 | cls = self.__class__.__name__ 520 | if self.path is not None: 521 | return "{}('{}')".format(cls, self.path) 522 | else: 523 | return "{}('{}', {})".format(cls, self.disk.path, self.partno) 524 | 525 | 526 | class CrosPartition(Partition): 527 | @property 528 | def attribute(self): 529 | return cgpt.get_raw_attribute(self.disk.path, self.partno) 530 | 531 | @attribute.setter 532 | def attribute(self, attr): 533 | return cgpt.set_raw_attribute(self.disk.path, self.partno, attr) 534 | 535 | @property 536 | def flags(self): 537 | flags = cgpt.get_flags(self.disk.path, self.partno) 538 | return { 539 | "attribute": flags["A"], 540 | "successful": flags["S"], 541 | "priority": flags["P"], 542 | "tries": flags["T"], 543 | } 544 | 545 | @flags.setter 546 | def flags(self, value): 547 | if isinstance(value, dict): 548 | A = value.get("attribute", None) 549 | S = value.get("successful", None) 550 | P = value.get("priority", None) 551 | T = value.get("tries", None) 552 | 553 | else: 554 | A = getattr(value, "attribute", None) 555 | S = getattr(value, "successful", None) 556 | P = getattr(value, "priority", None) 557 | T = getattr(value, "tries", None) 558 | 559 | cgpt.set_flags(self.disk.path, self.partno, A=A, S=S, P=P, T=T) 560 | 561 | @property 562 | def successful(self): 563 | return self.flags["successful"] 564 | 565 | @successful.setter 566 | def successful(self, value): 567 | self.flags = {"successful": value} 568 | 569 | @property 570 | def tries(self): 571 | return self.flags["tries"] 572 | 573 | @tries.setter 574 | def tries(self, value): 575 | self.flags = {"tries": value} 576 | 577 | @property 578 | def priority(self): 579 | return self.flags["priority"] 580 | 581 | @priority.setter 582 | def priority(self, value): 583 | self.flags = {"priority": value} 584 | 585 | def prioritize(self): 586 | return cgpt.prioritize(self.disk.path, self.partno) 587 | 588 | def _comparable_parts(self): 589 | flags = self.flags 590 | size = self.size 591 | 592 | return ( 593 | flags["successful"], 594 | flags["priority"], 595 | flags["tries"], 596 | self.size, 597 | ) 598 | 599 | def __lt__(self, other): 600 | if not isinstance(other, CrosPartition): 601 | return NotImplemented 602 | 603 | return self._comparable_parts() < other._comparable_parts() 604 | 605 | def __gt__(self, other): 606 | if not isinstance(other, CrosPartition): 607 | return NotImplemented 608 | 609 | return self._comparable_parts() > other._comparable_parts() 610 | -------------------------------------------------------------------------------- /depthcharge_tools/utils/pathlib.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | # depthcharge-tools pathlib utilities 5 | # Copyright (C) 2020-2022 Alper Nebi Yasak 6 | # See COPYRIGHT and LICENSE files for full copyright information. 7 | 8 | import shutil 9 | import subprocess 10 | 11 | from pathlib import Path 12 | 13 | from depthcharge_tools.utils.subprocess import ( 14 | gzip, 15 | lz4, 16 | lzma, 17 | lzop, 18 | bzip2, 19 | xz, 20 | zstd, 21 | ) 22 | 23 | def copy(src, dest): 24 | dest = shutil.copy2(src, dest) 25 | return Path(dest) 26 | 27 | 28 | def decompress(src, dest=None, partial=False): 29 | if dest is not None: 30 | dest = Path(dest) 31 | 32 | for runner in (gzip, zstd, xz, lz4, lzma, bzip2, lzop): 33 | try: 34 | return runner.decompress(src, dest) 35 | 36 | except FileNotFoundError: 37 | if dest: 38 | dest.unlink() 39 | 40 | except subprocess.CalledProcessError as err: 41 | if dest is None and err.output and partial: 42 | return err.output 43 | 44 | elif dest and dest.stat().st_size > 0 and partial: 45 | return dest 46 | 47 | elif dest: 48 | dest.unlink() 49 | 50 | 51 | def iterdir(path): 52 | try: 53 | if path.is_dir(): 54 | return path.iterdir() 55 | else: 56 | return [] 57 | except: 58 | return [] 59 | 60 | 61 | def read_lines(path): 62 | try: 63 | if path.is_file(): 64 | return path.read_text().splitlines() 65 | else: 66 | return [] 67 | except: 68 | return [] 69 | -------------------------------------------------------------------------------- /depthcharge_tools/utils/platform.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | # depthcharge-tools platform utilities 5 | # Copyright (C) 2020-2022 Alper Nebi Yasak 6 | # See COPYRIGHT and LICENSE files for full copyright information. 7 | 8 | import collections 9 | import glob 10 | import platform 11 | import re 12 | import shlex 13 | 14 | from pathlib import Path 15 | 16 | from depthcharge_tools.utils.pathlib import ( 17 | decompress, 18 | ) 19 | from depthcharge_tools.utils.subprocess import ( 20 | crossystem, 21 | ) 22 | 23 | 24 | def dt_compatibles(): 25 | dt_model = Path("/proc/device-tree/compatible") 26 | if dt_model.exists(): 27 | return dt_model.read_text().strip("\x00").split("\x00") 28 | 29 | 30 | def dt_model(): 31 | dt_model = Path("/proc/device-tree/model") 32 | if dt_model.exists(): 33 | return dt_model.read_text().strip("\x00") 34 | 35 | 36 | def cros_hwid(): 37 | hwid_file = Path("/proc/device-tree/firmware/chromeos/hardware-id") 38 | if hwid_file.exists(): 39 | return hwid_file.read_text().strip("\x00") 40 | 41 | for hwid_file in Path("/sys/bus/platform/devices").glob("GGL0001:*/HWID"): 42 | if hwid_file.exists(): 43 | return hwid_file.read_text().strip() 44 | 45 | # Try crossystem as a last resort 46 | try: 47 | return crossystem.hwid() 48 | except: 49 | pass 50 | 51 | 52 | def cros_fwid(): 53 | fwid_file = Path("/proc/device-tree/firmware/chromeos/firmware-version") 54 | if fwid_file.exists(): 55 | return fwid_file.read_text().strip("\x00") 56 | 57 | for fwid_file in Path("/sys/bus/platform/devices").glob("GGL0001:*/FWID"): 58 | if fwid_file.exists(): 59 | return fwid_file.read_text().strip() 60 | 61 | # Try crossystem as a last resort 62 | try: 63 | return crossystem.fwid() 64 | except: 65 | pass 66 | 67 | 68 | def os_release(root=None): 69 | os_release = {} 70 | 71 | if root is None: 72 | root = "/" 73 | root = Path(root).resolve() 74 | 75 | os_release_f = root / "etc" / "os-release" 76 | if not os_release_f.exists(): 77 | os_release_f = root / "usr" / "lib" / "os-release" 78 | 79 | if os_release_f.exists(): 80 | for line in os_release_f.read_text().splitlines(): 81 | lhs, _, rhs = line.partition("=") 82 | os_release[lhs] = rhs.strip('\'"') 83 | 84 | return os_release 85 | 86 | 87 | def kernel_cmdline(root=None): 88 | cmdline = "" 89 | 90 | if root is None: 91 | root = "/" 92 | root = Path(root).resolve() 93 | 94 | cmdline_f = root / "etc" / "kernel" / "cmdline" 95 | if not cmdline_f.exists(): 96 | cmdline_f = root / "usr" / "lib" / "kernel" / "cmdline" 97 | 98 | if cmdline_f.exists(): 99 | cmdline = cmdline_f.read_text().rstrip("\n") 100 | 101 | return shlex.split(cmdline) 102 | 103 | 104 | def proc_cmdline(): 105 | cmdline = "" 106 | 107 | cmdline_f = Path("/proc/cmdline") 108 | if cmdline_f.exists(): 109 | cmdline = cmdline_f.read_text().rstrip("\n") 110 | 111 | return shlex.split(cmdline) 112 | 113 | 114 | def is_cros_boot(): 115 | dt_cros_firmware = Path("/proc/device-tree/firmware/chromeos") 116 | if dt_cros_firmware.is_dir(): 117 | return True 118 | 119 | # Chrome OS firmware injects this into the kernel cmdline. 120 | if "cros_secure" in proc_cmdline(): 121 | return True 122 | 123 | return False 124 | 125 | 126 | def is_cros_libreboot(): 127 | fwid = cros_fwid() 128 | if fwid is None: 129 | return False 130 | 131 | return fwid.lower().startswith("libreboot") 132 | 133 | 134 | def root_requires_initramfs(root): 135 | x = "[0-9a-fA-F]" 136 | uuid = "{x}{{8}}-{x}{{4}}-{x}{{4}}-{x}{{4}}-{x}{{12}}".format(x=x) 137 | ntsig = "{x}{{8}}-{x}{{2}}".format(x=x) 138 | 139 | # Depthcharge replaces %U with an uuid, so we can use that as well. 140 | uuid = "({}|%U)".format(uuid) 141 | 142 | # Tries to validate the root=* kernel cmdline parameter. 143 | # See init/do_mounts.c in Linux tree. 144 | for pat in ( 145 | "[0-9a-fA-F]{4}", 146 | "/dev/nfs", 147 | "/dev/[0-9a-zA-Z]+", 148 | "/dev/[0-9a-zA-Z]+[0-9]+", 149 | "/dev/[0-9a-zA-Z]+p[0-9]+", 150 | "PARTUUID=({uuid}|{ntsig})".format(uuid=uuid, ntsig=ntsig), 151 | "PARTUUID=({uuid}|{ntsig})/PARTNROFF=[0-9]+".format( 152 | uuid=uuid, ntsig=ntsig, 153 | ), 154 | "[0-9]+:[0-9]+", 155 | "PARTLABEL=.+", 156 | "/dev/cifs", 157 | ): 158 | if re.fullmatch(pat, root): 159 | return False 160 | 161 | return True 162 | 163 | 164 | def vboot_keys(*keydirs, system=True, root=None): 165 | if len(keydirs) == 0 or system: 166 | if root is None: 167 | root = "/" 168 | root = Path(root).resolve() 169 | 170 | keydirs = ( 171 | *keydirs, 172 | root / "etc" / "depthcharge-tools", 173 | root / "usr" / "share" / "vboot" / "devkeys", 174 | root / "usr" / "local" / "share" / "vboot" / "devkeys", 175 | ) 176 | 177 | for keydir in keydirs: 178 | keydir = Path(keydir) 179 | if not keydir.is_dir(): 180 | continue 181 | 182 | keyblock = keydir / "kernel.keyblock" 183 | signprivate = keydir / "kernel_data_key.vbprivk" 184 | signpubkey = keydir / "kernel_subkey.vbpubk" 185 | 186 | if not keyblock.exists(): 187 | keyblock = None 188 | if not signprivate.exists(): 189 | signprivate = None 190 | if not signpubkey.exists(): 191 | signpubkey = None 192 | 193 | if keyblock or signprivate or signpubkey: 194 | return keydir, keyblock, signprivate, signpubkey 195 | 196 | return None, None, None, None 197 | 198 | 199 | def cpu_microcode(boot=None): 200 | microcode = [] 201 | 202 | for f in ( 203 | *boot.glob("amd-ucode.img"), 204 | *boot.glob("amd-uc.img"), 205 | ): 206 | if f.is_file(): 207 | microcode.append(f) 208 | break 209 | 210 | for f in ( 211 | *boot.glob("intel-ucode.img"), 212 | *boot.glob("intel-uc.img"), 213 | ): 214 | if f.is_file(): 215 | microcode.append(f) 216 | break 217 | 218 | if not microcode: 219 | for f in ( 220 | *boot.glob("early_ucode.cpio"), 221 | *boot.glob("microcode.cpio"), 222 | ): 223 | if f.is_file(): 224 | microcode.append(f) 225 | break 226 | 227 | return microcode 228 | 229 | 230 | def installed_kernels(root=None, boot=None): 231 | kernels = {} 232 | initrds = {} 233 | fdtdirs = {} 234 | 235 | if root is None: 236 | root = "/" 237 | root = Path(root).resolve() 238 | 239 | if boot is None: 240 | boot = root / "boot" 241 | boot = Path(boot).resolve() 242 | 243 | for f in ( 244 | *root.glob("lib/modules/*/vmlinuz"), 245 | *root.glob("lib/modules/*/vmlinux"), 246 | *root.glob("lib/modules/*/Image"), 247 | *root.glob("lib/modules/*/zImage"), 248 | *root.glob("lib/modules/*/bzImage"), 249 | *root.glob("usr/lib/modules/*/vmlinuz"), 250 | *root.glob("usr/lib/modules/*/vmlinux"), 251 | *root.glob("usr/lib/modules/*/Image"), 252 | *root.glob("usr/lib/modules/*/zImage"), 253 | *root.glob("usr/lib/modules/*/bzImage"), 254 | ): 255 | if not f.is_file(): 256 | continue 257 | release = f.parent.name 258 | kernels[release] = f.resolve() 259 | 260 | for f in ( 261 | *boot.glob("vmlinuz-*"), 262 | *boot.glob("vmlinux-*"), 263 | ): 264 | if not f.is_file(): 265 | continue 266 | _, _, release = f.name.partition("-") 267 | kernels[release] = f.resolve() 268 | 269 | for f in ( 270 | *boot.glob("vmlinuz"), 271 | *boot.glob("vmlinux"), 272 | *root.glob("vmlinuz"), 273 | *root.glob("vmlinux"), 274 | *boot.glob("Image"), 275 | *boot.glob("zImage"), 276 | *boot.glob("bzImage"), 277 | ): 278 | if not f.is_file(): 279 | continue 280 | kernels[None] = f.resolve() 281 | break 282 | 283 | for f in ( 284 | *root.glob("lib/modules/*/initrd"), 285 | *root.glob("lib/modules/*/initramfs"), 286 | *root.glob("lib/modules/*/initrd.img"), 287 | *root.glob("lib/modules/*/initramfs.img"), 288 | *root.glob("usr/lib/modules/*/initrd"), 289 | *root.glob("usr/lib/modules/*/initramfs"), 290 | *root.glob("usr/lib/modules/*/initrd.img"), 291 | *root.glob("usr/lib/modules/*/initramfs.img"), 292 | ): 293 | if not f.is_file(): 294 | continue 295 | release = f.parent.name 296 | initrds[release] = f.resolve() 297 | 298 | for f in ( 299 | *boot.glob("initrd-*.img"), 300 | *boot.glob("initramfs-*.img"), 301 | ): 302 | if not f.is_file(): 303 | continue 304 | _, _, release = f.name.partition("-") 305 | release = release[:-4] 306 | initrds[release] = f.resolve() 307 | 308 | for f in ( 309 | *boot.glob("initrd-*"), 310 | *boot.glob("initrd.img-*"), 311 | *boot.glob("initramfs-*"), 312 | *boot.glob("initramfs.img-*"), 313 | ): 314 | if not f.is_file(): 315 | continue 316 | _, _, release = f.name.partition("-") 317 | initrds[release] = f.resolve() 318 | 319 | for f in ( 320 | *boot.glob("initrd.img"), 321 | *boot.glob("initrd"), 322 | *boot.glob("initramfs-linux.img"), 323 | *boot.glob("initramfs-vanilla"), 324 | *boot.glob("initramfs"), 325 | *root.glob("initrd.img"), 326 | *root.glob("initrd"), 327 | *root.glob("initramfs"), 328 | ): 329 | if not f.is_file(): 330 | continue 331 | initrds[None] = f.resolve() 332 | break 333 | 334 | for d in ( 335 | *root.glob("usr/lib/linux-image-*"), 336 | ): 337 | if not d.is_dir(): 338 | continue 339 | _, _, release = d.name.partition("linux-image-") 340 | fdtdirs[release] = d.resolve() 341 | 342 | for d in ( 343 | *root.glob("lib/modules/*/dtb"), 344 | *root.glob("lib/modules/*/dtbs"), 345 | *root.glob("usr/lib/modules/*/dtb"), 346 | *root.glob("usr/lib/modules/*/dtbs"), 347 | ): 348 | if not d.is_dir(): 349 | continue 350 | release = d.parent.name 351 | fdtdirs[release] = d.resolve() 352 | 353 | for d in ( 354 | *boot.glob("dtb-*"), 355 | *boot.glob("dtbs-*"), 356 | ): 357 | if not d.is_dir(): 358 | continue 359 | _, _, release = d.name.partition("-") 360 | fdtdirs[release] = d.resolve() 361 | 362 | for d in ( 363 | *boot.glob("dtb/*"), 364 | *boot.glob("dtbs/*"), 365 | ): 366 | if not d.is_dir(): 367 | continue 368 | if d.name in kernels: 369 | fdtdirs[d.name] = d.resolve() 370 | 371 | for d in ( 372 | *boot.glob("dtbs"), 373 | *boot.glob("dtb"), 374 | *root.glob("usr/share/dtbs"), 375 | *root.glob("usr/share/dtb"), 376 | ): 377 | if not d.is_dir(): 378 | continue 379 | # Duplicate dtb files means that the directory is split by 380 | # kernel release and we can't use it for a single release. 381 | dtbs = d.glob("**/*.dtb") 382 | counts = collections.Counter(dtb.name for dtb in dtbs) 383 | if all(c <= 1 for c in counts.values()): 384 | fdtdirs[None] = d.resolve() 385 | break 386 | 387 | if None in kernels: 388 | kernel, release = kernels[None], None 389 | for r, k in kernels.items(): 390 | if k == kernel and r is not None: 391 | release = r 392 | break 393 | 394 | if release is not None: 395 | del kernels[None] 396 | if None in initrds: 397 | initrds.setdefault(release, initrds[None]) 398 | del initrds[None] 399 | if None in fdtdirs: 400 | fdtdirs.setdefault(release, fdtdirs[None]) 401 | del fdtdirs[None] 402 | 403 | return [ 404 | KernelEntry( 405 | release, 406 | kernel=kernels[release], 407 | initrd=initrds.get(release, None), 408 | fdtdir=fdtdirs.get(release, None), 409 | os_name=os_release(root=root).get("NAME", None), 410 | ) for release in kernels.keys() 411 | ] 412 | 413 | 414 | class KernelEntry: 415 | def __init__(self, release, kernel, initrd=None, fdtdir=None, os_name=None): 416 | self.release = release 417 | self.kernel = kernel 418 | self.initrd = initrd 419 | self.fdtdir = fdtdir 420 | self.os_name = os_name 421 | 422 | @property 423 | def description(self): 424 | if self.os_name is None: 425 | return "Linux {}".format(self.release) 426 | else: 427 | return "{}, with Linux {}".format(self.os_name, self.release) 428 | 429 | @property 430 | def arch(self): 431 | kernel = Path(self.kernel) 432 | 433 | decomp = decompress(kernel) 434 | if decomp: 435 | head = decomp[:4096] 436 | else: 437 | with kernel.open("rb") as f: 438 | head = f.read(4096) 439 | 440 | if head[0x202:0x206] == b"HdrS": 441 | return Architecture("x86") 442 | elif head[0x38:0x3c] == b"ARM\x64": 443 | return Architecture("arm64") 444 | elif head[0x34:0x38] == b"\x45\x45\x45\x45": 445 | return Architecture("arm") 446 | 447 | def _comparable_parts(self): 448 | pattern = "([^a-zA-Z0-9]?)([a-zA-Z]*)([0-9]*)" 449 | 450 | if self.release is None: 451 | return () 452 | 453 | parts = [] 454 | for sep, text, num in re.findall(pattern, self.release): 455 | # x.y.z > x.y-* == x.y* > x.y~* 456 | sep = { 457 | "~": -1, 458 | ".": 1, 459 | }.get(sep, 0) 460 | 461 | # x.y-* == x.y* > x.y > x.y-rc* == x.y-trunk* 462 | text = ({ 463 | "rc": -1, 464 | "trunk": -1, 465 | }.get(text, 0), text) 466 | 467 | # Compare numbers as numbers 468 | num = int(num) if num else 0 469 | 470 | parts.append((sep, text, num)) 471 | 472 | return tuple(parts) 473 | 474 | def __lt__(self, other): 475 | if not isinstance(other, KernelEntry): 476 | return NotImplemented 477 | 478 | return self._comparable_parts() < other._comparable_parts() 479 | 480 | def __gt__(self, other): 481 | if not isinstance(other, KernelEntry): 482 | return NotImplemented 483 | 484 | return self._comparable_parts() > other._comparable_parts() 485 | 486 | def __str__(self): 487 | return self.description 488 | 489 | def __repr__(self): 490 | return ( 491 | "KernelEntry(release={!r}, kernel={!r}, initrd={!r}, fdtdir={!r}, os_name={!r})" 492 | .format(self.release, self.kernel, self.initrd, self.fdtdir, self.os_name) 493 | ) 494 | 495 | 496 | class Architecture(str): 497 | arm_32 = ["arm", "ARM", "armv7", "ARMv7", ] 498 | arm_64 = ["arm64", "ARM64", "aarch64", "AArch64"] 499 | arm = arm_32 + arm_64 500 | x86_32 = ["i386", "x86"] 501 | x86_64 = ["x86_64", "amd64", "AMD64"] 502 | x86 = x86_32 + x86_64 503 | all = arm + x86 504 | groups = (arm_32, arm_64, x86_32, x86_64) 505 | 506 | def __eq__(self, other): 507 | if isinstance(other, Architecture): 508 | for group in self.groups: 509 | if self in group and other in group: 510 | return True 511 | return str(self) == str(other) 512 | 513 | def __ne__(self, other): 514 | if isinstance(other, Architecture): 515 | for group in self.groups: 516 | if self in group and other not in group: 517 | return True 518 | return str(self) != str(other) 519 | 520 | @property 521 | def mkimage(self): 522 | if self in self.arm_32: 523 | return "arm" 524 | if self in self.arm_64: 525 | return "arm64" 526 | if self in self.x86_32: 527 | return "x86" 528 | if self in self.x86_64: 529 | return "x86_64" 530 | 531 | @property 532 | def vboot(self): 533 | if self in self.arm_32: 534 | return "arm" 535 | if self in self.arm_64: 536 | return "aarch64" 537 | if self in self.x86_32: 538 | return "x86" 539 | if self in self.x86_64: 540 | return "amd64" 541 | 542 | @property 543 | def kernel_arches(self): 544 | if self in self.arm_32: 545 | return self.arm_32 546 | if self in self.arm_64: 547 | return self.arm 548 | if self in self.x86_32: 549 | return self.x86_32 550 | if self in self.x86_64: 551 | return self.x86 552 | -------------------------------------------------------------------------------- /depthcharge_tools/utils/string.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | # depthcharge-tools string utilities 5 | # Copyright (C) 2022 Alper Nebi Yasak 6 | # See COPYRIGHT and LICENSE files for full copyright information. 7 | 8 | import ast 9 | import re 10 | 11 | def bytesize_suffixes(): 12 | def long_forms(x): 13 | formats = ("{}", "{}byte", "{} byte", "{}bytes", "{} bytes") 14 | cases = (str.upper, str.lower, str.title) 15 | for f in formats: 16 | for c in cases: 17 | yield c(f.format(x)) 18 | 19 | for size, suffixes in { 20 | 1: ("B", "byte", "bytes", ""), 21 | 1e3: ("kB", "KB", *long_forms("kilo")), 22 | 1e6: ("MB", *long_forms("mega")), 23 | 1e9: ("GB", *long_forms("giga")), 24 | 1e12: ("TB", *long_forms("tera")), 25 | 1e15: ("PB", *long_forms("peta")), 26 | 1e18: ("EB", *long_forms("exa")), 27 | 1e21: ("ZB", *long_forms("zetta")), 28 | 1e24: ("YB", *long_forms("yotta")), 29 | 2 ** 10: ("kiB", "KiB", "K", *long_forms("kibi")), 30 | 2 ** 20: ("MiB", "M", *long_forms("mebi")), 31 | 2 ** 30: ("GiB", "G", *long_forms("gibi")), 32 | 2 ** 40: ("TiB", "T", *long_forms("tebi")), 33 | 2 ** 50: ("PiB", "P", *long_forms("pebi")), 34 | 2 ** 60: ("EiB", "E", *long_forms("exbi")), 35 | 2 ** 70: ("ZiB", "Z", *long_forms("zebi")), 36 | 2 ** 80: ("YiB", "Y", *long_forms("yobi")), 37 | }.items(): 38 | for suffix in suffixes: 39 | yield (suffix.strip(), int(size)) 40 | 41 | bytesize_suffixes = dict(bytesize_suffixes()) 42 | 43 | 44 | def parse_bytesize(val): 45 | if val is None: 46 | return None 47 | 48 | try: 49 | return int(val) 50 | except: 51 | pass 52 | 53 | try: 54 | return int(ast.literal_eval(val)) 55 | except: 56 | pass 57 | 58 | try: 59 | s = str(val) 60 | suffix = re.search("[a-zA-Z\s]*\Z", s)[0].strip() 61 | number = s.rpartition(suffix)[0].strip() 62 | multiplier = bytesize_suffixes[suffix] 63 | return int(ast.literal_eval(number)) * multiplier 64 | 65 | except Exception as err: 66 | raise ValueError( 67 | "Cannot convert '{}' to a byte-size." 68 | .format(val) 69 | ) 70 | -------------------------------------------------------------------------------- /depthcharge_tools/utils/subprocess.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | # depthcharge-tools subprocess utilities 5 | # Copyright (C) 2020-2022 Alper Nebi Yasak 6 | # See COPYRIGHT and LICENSE files for full copyright information. 7 | 8 | import contextlib 9 | import logging 10 | import re 11 | import subprocess 12 | import shlex 13 | 14 | from pathlib import Path 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | class ProcessRunner: 20 | def __init__(self, *args_prefix, **kwargs_defaults): 21 | self.args_prefix = args_prefix 22 | self.kwargs_defaults = { 23 | 'encoding': "utf-8", 24 | 'check': True, 25 | } 26 | self.kwargs_defaults.update(kwargs_defaults) 27 | 28 | def __call__(self, *args_suffix, **kwargs_overrides): 29 | args = (*self.args_prefix, *args_suffix) 30 | kwargs = {**self.kwargs_defaults, **kwargs_overrides} 31 | 32 | with contextlib.ExitStack() as ctx: 33 | stdin = kwargs.get("stdin", None) 34 | if isinstance(stdin, str): 35 | stdin = Path(stdin) 36 | if isinstance(stdin, bytes): 37 | kwargs["stdin"] = None 38 | kwargs["encoding"] = None 39 | kwargs["input"] = stdin 40 | if isinstance(stdin, Path): 41 | kwargs["stdin"] = ctx.enter_context(stdin.open("r")) 42 | if stdin is None: 43 | kwargs["stdin"] = subprocess.PIPE 44 | 45 | stdout = kwargs.get("stdout", None) 46 | if isinstance(stdout, str): 47 | stdout = Path(stdout) 48 | if isinstance(stdout, Path): 49 | kwargs["stdout"] = ctx.enter_context(stdout.open("x")) 50 | if stdout is None: 51 | kwargs["stdout"] = subprocess.PIPE 52 | 53 | stderr = kwargs.get("stderr", None) 54 | if isinstance(stderr, str): 55 | stderr = Path(stderr) 56 | if isinstance(stderr, Path): 57 | kwargs["stderr"] = ctx.enter_context(stderr.open("x")) 58 | if stderr is None: 59 | kwargs["stderr"] = subprocess.PIPE 60 | 61 | try: 62 | return subprocess.run(args, **kwargs) 63 | except subprocess.CalledProcessError as err: 64 | our_err = self._parse_subprocess_error(err) 65 | if our_err is None: 66 | return subprocess.CompletedProcess( 67 | args=err.cmd, 68 | returncode=err.returncode, 69 | stdout=err.stdout, 70 | stderr=err.stderr, 71 | ) 72 | if our_err is not err: 73 | raise our_err 74 | raise 75 | 76 | def _parse_subprocess_error(self, err): 77 | return err 78 | 79 | 80 | class GzipRunner(ProcessRunner): 81 | def __init__(self): 82 | super().__init__("gzip", encoding=None) 83 | 84 | def compress(self, src, dest=None): 85 | proc = self("-c", "-6", stdin=src, stdout=dest) 86 | 87 | if dest is None: 88 | return proc.stdout 89 | else: 90 | return Path(dest) 91 | 92 | def decompress(self, src, dest=None): 93 | proc = self("-c", "-d", stdin=src, stdout=dest) 94 | 95 | if dest is None: 96 | return proc.stdout 97 | else: 98 | return Path(dest) 99 | 100 | def test(self, path): 101 | proc = self("-t", stdin=path, check=False) 102 | return proc.returncode == 0 103 | 104 | 105 | class Lz4Runner(ProcessRunner): 106 | def __init__(self): 107 | super().__init__("lz4", encoding=None) 108 | 109 | def compress(self, src, dest=None): 110 | proc = self("-z", "-9", stdin=src, stdout=dest) 111 | 112 | if dest is None: 113 | return proc.stdout 114 | else: 115 | return Path(dest) 116 | 117 | def decompress(self, src, dest=None): 118 | proc = self("-d", stdin=src, stdout=dest) 119 | 120 | if dest is None: 121 | return proc.stdout 122 | else: 123 | return Path(dest) 124 | 125 | def test(self, path): 126 | proc = self("-t", stdin=path, check=False) 127 | return proc.returncode == 0 128 | 129 | 130 | class LzmaRunner(ProcessRunner): 131 | def __init__(self): 132 | super().__init__("lzma", encoding=None) 133 | 134 | def compress(self, src, dest=None): 135 | proc = self("-z", stdin=src, stdout=dest) 136 | 137 | if dest is None: 138 | return proc.stdout 139 | else: 140 | return Path(dest) 141 | 142 | def decompress(self, src, dest=None): 143 | proc = self("-d", stdin=src, stdout=dest) 144 | 145 | if dest is None: 146 | return proc.stdout 147 | else: 148 | return Path(dest) 149 | 150 | def test(self, path): 151 | proc = self("-t", stdin=path, check=False) 152 | return proc.returncode == 0 153 | 154 | 155 | class LzopRunner(ProcessRunner): 156 | def __init__(self): 157 | super().__init__("lzop", encoding=None) 158 | 159 | def compress(self, src, dest=None): 160 | proc = self("-c", stdin=src, stdout=dest) 161 | 162 | if dest is None: 163 | return proc.stdout 164 | else: 165 | return Path(dest) 166 | 167 | def decompress(self, src, dest=None): 168 | proc = self("-c", "-d", stdin=src, stdout=dest) 169 | 170 | if dest is None: 171 | return proc.stdout 172 | else: 173 | return Path(dest) 174 | 175 | def test(self, path): 176 | proc = self("-t", stdin=path, check=False) 177 | return proc.returncode == 0 178 | 179 | 180 | class Bzip2Runner(ProcessRunner): 181 | def __init__(self): 182 | super().__init__("bzip2", encoding=None) 183 | 184 | def compress(self, src, dest=None): 185 | proc = self("-c", stdin=src, stdout=dest) 186 | 187 | if dest is None: 188 | return proc.stdout 189 | else: 190 | return Path(dest) 191 | 192 | def decompress(self, src, dest=None): 193 | proc = self("-c", "-d", stdin=src, stdout=dest) 194 | 195 | if dest is None: 196 | return proc.stdout 197 | else: 198 | return Path(dest) 199 | 200 | def test(self, path): 201 | proc = self("-t", stdin=path, check=False) 202 | return proc.returncode == 0 203 | 204 | 205 | class XzRunner(ProcessRunner): 206 | def __init__(self): 207 | super().__init__("xz", encoding=None) 208 | 209 | def compress(self, src, dest=None): 210 | proc = self("-z", "--check=crc32", stdin=src, stdout=dest) 211 | 212 | if dest is None: 213 | return proc.stdout 214 | else: 215 | return Path(dest) 216 | 217 | def decompress(self, src, dest=None): 218 | proc = self("-d", stdin=src, stdout=dest) 219 | 220 | if dest is None: 221 | return proc.stdout 222 | else: 223 | return Path(dest) 224 | 225 | def test(self, path): 226 | proc = self("-t", stdin=path, check=False) 227 | return proc.returncode == 0 228 | 229 | 230 | class ZstdRunner(ProcessRunner): 231 | def __init__(self): 232 | super().__init__("zstd", encoding=None) 233 | 234 | def compress(self, src, dest=None): 235 | proc = self("-z", "-9", stdin=src, stdout=dest) 236 | 237 | if dest is None: 238 | return proc.stdout 239 | else: 240 | return Path(dest) 241 | 242 | def decompress(self, src, dest=None): 243 | proc = self("-d", stdin=src, stdout=dest) 244 | 245 | if dest is None: 246 | return proc.stdout 247 | else: 248 | return Path(dest) 249 | 250 | def test(self, path): 251 | proc = self("-t", stdin=path, check=False) 252 | return proc.returncode == 0 253 | 254 | 255 | class MkimageRunner(ProcessRunner): 256 | def __init__(self): 257 | super().__init__("mkimage") 258 | 259 | 260 | class VbutilKernelRunner(ProcessRunner): 261 | def __init__(self): 262 | super().__init__("futility", "vbutil_kernel") 263 | 264 | 265 | class CgptRunner(ProcessRunner): 266 | def __init__(self): 267 | super().__init__("cgpt") 268 | 269 | def __call__(self, *args, **kwargs): 270 | proc = super().__call__(*args, **kwargs) 271 | lines = proc.stdout.splitlines() 272 | 273 | # Sometimes cgpt prints duplicate output. 274 | # https://bugs.chromium.org/p/chromium/issues/detail?id=463414 275 | mid = len(lines) // 2 276 | if lines[:mid] == lines[mid:]: 277 | proc.stdout = "\n".join(lines[:mid]) 278 | 279 | return proc 280 | 281 | def _parse_subprocess_error(self, err): 282 | # Exits with nonzero status if it finds no partitions of 283 | # given type even if the disk has a valid partition table 284 | if not err.stderr: 285 | return None 286 | 287 | m = re.fullmatch( 288 | "ERROR: Can't open (.*): Permission denied\n", 289 | err.stderr, 290 | ) 291 | if m: 292 | return PermissionError( 293 | "Couldn't open '{}', permission denied." 294 | .format(m.groups()[0]) 295 | ) 296 | 297 | return err 298 | 299 | 300 | def get_raw_attribute(self, disk, partno): 301 | proc = self("show", "-A", "-i", str(partno), str(disk)) 302 | attribute = int(proc.stdout, 16) 303 | return attribute 304 | 305 | def set_raw_attribute(self, disk, partno, attribute): 306 | self("add", "-A", hex(attribute), "-i", str(partno), str(disk)) 307 | 308 | def get_flags(self, disk, partno): 309 | attribute = self.get_raw_attribute(disk, partno) 310 | successful = (attribute >> 8) & 0x1 311 | tries = (attribute >> 4) & 0xF 312 | priority = (attribute >> 0) & 0xF 313 | 314 | return { 315 | "A": attribute, 316 | "S": successful, 317 | "P": priority, 318 | "T": tries, 319 | } 320 | 321 | def set_flags(self, disk, partno, A=None, S=None, P=None, T=None): 322 | flag_args = [] 323 | if A is not None: 324 | flag_args += ["-A", str(int(A))] 325 | if S is not None: 326 | flag_args += ["-S", str(int(S))] 327 | if P is not None: 328 | flag_args += ["-P", str(int(P))] 329 | if T is not None: 330 | flag_args += ["-T", str(int(T))] 331 | 332 | self("add", *flag_args, "-i", str(partno), str(disk)) 333 | 334 | def get_size(self, disk, partno): 335 | proc = self("show", "-s", "-i", str(partno), str(disk)) 336 | blocks = int(proc.stdout) 337 | return blocks * 512 338 | 339 | def get_start(self, disk, partno): 340 | proc = self("show", "-b", "-i", str(partno), str(disk)) 341 | blocks = int(proc.stdout) 342 | return blocks * 512 343 | 344 | def find_partitions(self, disk, type=None): 345 | if type is None: 346 | # cgpt find needs at least one of -t, -u, -l 347 | proc = self("show", "-q", "-n", disk) 348 | lines = proc.stdout.splitlines() 349 | partnos = [int(shlex.split(line)[2]) for line in lines] 350 | 351 | else: 352 | proc = self("find", "-n", "-t", type, disk) 353 | partnos = [int(n) for n in proc.stdout.splitlines()] 354 | 355 | return partnos 356 | 357 | def prioritize(self, disk, partno): 358 | self("prioritize", "-i", str(partno), str(disk)) 359 | 360 | 361 | class CrossystemRunner(ProcessRunner): 362 | def __init__(self): 363 | super().__init__("crossystem") 364 | 365 | def hwid(self): 366 | proc = self("hwid", check=False) 367 | 368 | if proc.returncode == 0: 369 | return proc.stdout 370 | else: 371 | return None 372 | 373 | def fwid(self): 374 | proc = self("fwid", check=False) 375 | 376 | if proc.returncode == 0: 377 | return proc.stdout 378 | else: 379 | return None 380 | 381 | 382 | class FdtgetRunner(ProcessRunner): 383 | def __init__(self): 384 | super().__init__("fdtget") 385 | 386 | def get(self, dt_file, node='/', prop='', default=None, type=None): 387 | options = [] 388 | 389 | if default is not None: 390 | options += ["--default", str(default)] 391 | 392 | if type == str: 393 | options += ["--type", "s"] 394 | elif type == int: 395 | options += ["--type", "i"] 396 | elif type == bytes: 397 | options += ["--type", 'bx'] 398 | elif type is not None: 399 | options += ["--type", str(type)] 400 | 401 | proc = self(*options, str(dt_file), str(node), str(prop)) 402 | 403 | # str.split takes too much memory 404 | def split(s): 405 | for m in re.finditer("(\S*)\s", s): 406 | yield m.group() 407 | 408 | if type in (None, int): 409 | try: 410 | data = [int(i) for i in split(proc.stdout)] 411 | return data[0] if len(data) == 1 else data 412 | except: 413 | pass 414 | 415 | if type in (None, bytes): 416 | try: 417 | # bytes.fromhex("0") doesn't work 418 | data = bytes(int(x, 16) for x in split(proc.stdout)) 419 | return data 420 | except: 421 | pass 422 | 423 | data = str(proc.stdout).strip("\n") 424 | return data 425 | 426 | def properties(self, dt_file, node='/'): 427 | proc = self("--properties", str(dt_file), str(node), check=False) 428 | 429 | if proc.returncode == 0: 430 | return proc.stdout.splitlines() 431 | else: 432 | return [] 433 | 434 | def subnodes(self, dt_file, node='/'): 435 | proc = self("--list", str(dt_file), str(node), check=False) 436 | nodes = proc.stdout.splitlines() 437 | 438 | if proc.returncode == 0: 439 | return proc.stdout.splitlines() 440 | else: 441 | return [] 442 | 443 | 444 | class FdtputRunner(ProcessRunner): 445 | def __init__(self): 446 | super().__init__("fdtput") 447 | 448 | def put(self, dt_file, node='/', prop='', value=None, type=None): 449 | if isinstance(value, list): 450 | values = value 451 | else: 452 | values = [value] 453 | 454 | value_args = [] 455 | for value in values: 456 | if isinstance(value, str): 457 | value_args.append(value) 458 | if type is None: 459 | type = str 460 | 461 | elif isinstance(value, bytes): 462 | value_args.extend(hex(c) for c in value) 463 | if type is None: 464 | type = bytes 465 | 466 | elif isinstance(value, int): 467 | value_args.append(str(value)) 468 | if type is None: 469 | type = int 470 | 471 | else: 472 | value_args.append(str(value)) 473 | 474 | options = [] 475 | if type == str: 476 | options += ["--type", "s"] 477 | elif type == int: 478 | options += ["--type", "i"] 479 | elif type == bytes: 480 | options += ["--type", 'bx'] 481 | elif type is not None: 482 | options += ["--type", str(type)] 483 | 484 | self(*options, str(dt_file), str(node), str(prop), *value_args) 485 | 486 | 487 | class FileRunner(ProcessRunner): 488 | def __init__(self): 489 | super().__init__("file") 490 | 491 | def brief(self, path): 492 | proc = self("-b", path, check=False) 493 | 494 | if proc.returncode == 0: 495 | return proc.stdout.strip("\n") 496 | else: 497 | return None 498 | 499 | 500 | gzip = GzipRunner() 501 | lz4 = Lz4Runner() 502 | lzma = LzmaRunner() 503 | lzop = LzopRunner() 504 | bzip2 = Bzip2Runner() 505 | xz = XzRunner() 506 | zstd = ZstdRunner() 507 | mkimage = MkimageRunner() 508 | vbutil_kernel = VbutilKernelRunner() 509 | cgpt = CgptRunner() 510 | crossystem = CrossystemRunner() 511 | fdtget = FdtgetRunner() 512 | fdtput = FdtputRunner() 513 | file = FileRunner() 514 | -------------------------------------------------------------------------------- /init.d/depthchargectl-bless: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | # depthcharge-tools depthchargectl-bless sysvinit service 5 | # Copyright (C) 2020-2021 Alper Nebi Yasak 6 | # See COPYRIGHT and LICENSE files for full copyright information. 7 | 8 | ### BEGIN INIT INFO 9 | # Provides: depthchargectl-bless 10 | # Required-Start: $remote_fs 11 | # Required-Stop: 12 | # Default-Start: 2 3 4 5 13 | # Default-Stop: 14 | # Short-Description: Mark the current depthcharge partition as successful 15 | ### END INIT INFO 16 | 17 | if ! command -v depthchargectl >/dev/null 2>/dev/null; then 18 | exit 0 19 | fi 20 | 21 | if ! grep "cros_secure" /proc/cmdline >/dev/null 2>/dev/null; then 22 | # Not booted by depthcharge. 23 | exit 0 24 | fi 25 | 26 | if [ -f /lib/lsb/init-functions ]; then 27 | . /lib/lsb/init-functions 28 | fi 29 | 30 | case "$1" in 31 | start|restart|reload|force-reload) 32 | depthchargectl bless 33 | ;; 34 | stop|status) 35 | # Not a daemon. 36 | ;; 37 | esac 38 | -------------------------------------------------------------------------------- /mkdepthcharge.rst: -------------------------------------------------------------------------------- 1 | .. SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | .. depthcharge-tools mkdepthcharge(1) manual page 4 | .. Copyright (C) 2019-2022 Alper Nebi Yasak 5 | .. See COPYRIGHT and LICENSE files for full copyright information. 6 | 7 | ============= 8 | mkdepthcharge 9 | ============= 10 | 11 | --------------------------------------------- 12 | Build boot images for the ChromeOS bootloader 13 | --------------------------------------------- 14 | 15 | :date: 2023-06-30 16 | :version: v0.6.2 17 | :manual_section: 1 18 | :manual_group: depthcharge-tools 19 | 20 | .. |depthchargectl| replace:: *depthchargectl*\ (1) 21 | .. |mkimage| replace:: *mkimage*\ (1) 22 | .. |vbutil_kernel| replace:: *vbutil_kernel*\ (1) 23 | .. |futility| replace:: *futility*\ (1) 24 | 25 | .. |CONFIG_DIR| replace:: **/etc/depthcharge-tools** 26 | .. |CONFIG_FILE| replace:: **/etc/depthcharge-tools/config** 27 | .. |CONFIGD_DIR| replace:: **/etc/depthcharge-tools/config.d** 28 | .. |VBOOT_DEVKEYS| replace:: **/usr/share/vboot/devkeys** 29 | .. |VBOOT_KEYBLOCK| replace:: **kernel.keyblock** 30 | .. |VBOOT_SIGNPUBKEY| replace:: **kernel_subkey.vbpubk** 31 | .. |VBOOT_SIGNPRIVATE| replace:: **kernel_data_key.vbprivk** 32 | 33 | SYNOPSIS 34 | ======== 35 | **mkdepthcharge** **-o** *FILE* [options] [*VMLINUZ*] [*INITRAMFS* ...] [*DTB* ...] 36 | 37 | 38 | DESCRIPTION 39 | =========== 40 | **mkdepthcharge** wraps the |mkimage| and |vbutil_kernel| 41 | programs with reasonable defaults to package its inputs into the 42 | format the ChromeOS bootloader expects. It also automates preprocessing 43 | steps and initramfs support hacks that a user would have to do manually 44 | or write a script for. 45 | 46 | The *VMLINUZ* should be a kernel executable, *INITRAMFS* should be a 47 | ramdisk image that the kernel should be able to use on its own, and 48 | *DTB* files should be device-tree binary files appropriate for the 49 | kernel. 50 | 51 | **mkdepthcharge** tries to determine the type of each input file by some 52 | heuristics on their contents, but failing that it assumes a file is 53 | whatever is missing in the *VMLINUZ*, *INITRAMFS*, *DTB* order. 54 | Alternatively, these files can be specified as options instead of 55 | positional arguments. 56 | 57 | 58 | OPTIONS 59 | ======= 60 | 61 | Input files 62 | ----------- 63 | 64 | -d VMLINUZ, --vmlinuz VMLINUZ 65 | Kernel executable. If a compressed file is given here, it is 66 | decompressed and its contents are used in its place. 67 | 68 | -i *INITRAMFS* [*INITRAMFS* ...], --initramfs *INITRAMFS* [*INITRAMFS* ...] 69 | Ramdisk image. If multiple files are given (e.g. for CPU microcode 70 | updates), they are concatenated and used as a single file. 71 | 72 | -b *DTB* [*DTB* ...], --dtbs *DTB* [*DTB* ...] 73 | Device-tree binary files. 74 | 75 | Global options 76 | -------------- 77 | -A ARCH, --arch ARCH 78 | Architecture to build the images for. The following architectures 79 | are understood: **arm**, **arm64**, **aarch64** for ARM boards; 80 | **x86**, **x86_64**, **amd64** for x86 boards. If not given, the 81 | build architecture of the *VMLINUZ* file is used. 82 | 83 | --format FORMAT 84 | Kernel image format to use, either **fit** or **zimage**. If not 85 | given, architecture-specific defaults are used. 86 | 87 | fit 88 | This is the default on ARM boards. The *VMLINUZ* and the 89 | optional *INITRAMFS*, *DTB* files are packaged into the 90 | Flattened Image Tree (FIT) format using |mkimage| and that is 91 | passed to |vbutil_kernel|. 92 | 93 | zimage 94 | This is the default for x86 boards. The *VMLINUZ* is passed 95 | mostly unmodified to |vbutil_kernel|, except for decompression 96 | and padding for self-decompression. The *INITRAMFS* file is 97 | passed as the **--bootloader** argument and the kernel header is 98 | modified to point to where it will be in memory. It does not 99 | support packaging *DTB* files. 100 | 101 | -h, --help 102 | Show a help message and exit. 103 | 104 | --kernel-start ADDR 105 | Start of the Depthcharge kernel buffer in memory. Depthcharge loads 106 | the packed data to a fixed physical address in memory, and some 107 | initramfs support hacks require this value to be known. This is 108 | exactly the board-specific **CONFIG_KERNEL_START** value in the 109 | Depthcharge source code and defaults to **0x100000** for the x86 110 | architecture. 111 | 112 | -o FILE, --output FILE 113 | Write the image to *FILE*. The image isn't generated at the output, 114 | but copied to it from a temporary working directory. This option is 115 | mandatory. 116 | 117 | --pad-vmlinuz, --no-pad-vmlinuz 118 | Pad the *VMLINUZ* file so that the kernel's self-decompression has 119 | enough space to avoid overwriting the *INITRAMFS* file during boot. 120 | This has different defaults and behaviour depending on the image 121 | format, see explanations in their respective sections. 122 | 123 | --tmpdir DIR 124 | Create and keep temporary files in *DIR*. If not given, a temporary 125 | **mkdepthcharge-\*** directory is created in **/tmp** and removed at 126 | exit. 127 | 128 | -v, --verbose 129 | Print info messages, |mkimage| output and |vbutil_kernel| output to 130 | stderr. 131 | 132 | -V, --version 133 | Print program version and exit. 134 | 135 | FIT image options 136 | ----------------- 137 | -C TYPE, --compress TYPE 138 | Compress the *VMLINUZ* before packaging it into a FIT image, either 139 | with **lz4** or **lzma**. **none** is also accepted, but does 140 | nothing. 141 | 142 | -n DESC, --name DESC 143 | Description of the *VMLINUZ* to put in the FIT image. 144 | 145 | --pad-vmlinuz, --no-pad-vmlinuz 146 | Pad the *VMLINUZ* file so that the kernel's self-decompression has 147 | enough space to avoid overwriting the *INITRAMFS* file during boot. 148 | The necessary padding is calculated based on compressed and 149 | decompressed kernel sizes and the **--kernel-start** argument. 150 | 151 | On earliest boards U-Boot moves the *INITRAMFS* away to a safe place 152 | before running the *VMLINUZ*, and on ARM64 boards Depthcharge itself 153 | decompresses the *VMLINUZ* to a safe place. But 32-bit ARM boards 154 | with Depthcharge lack FIT ramdisk support and run the *VMLINUZ* 155 | in-place, so this initramfs support hack is necessary on those. 156 | 157 | This option is enabled by default when **--patch-dtbs** is given, 158 | use the **--no-pad-vmlinuz** argument to disable it. 159 | 160 | --patch-dtbs, --no-patch-dtbs 161 | Add **linux,initrd-start** and **linux,initrd-end** properties to 162 | the *DTB* files' **/chosen** nodes. Their values are based on the 163 | **--kernel-start** or the **--ramdisk-load-address** argument, one 164 | of which is required if this argument is given. 165 | 166 | These properties are normally added by Depthcharge, but 32-bit ARM 167 | Chromebooks were released with versions before FIT ramdisk support 168 | was introduced, so this initramfs support hack is necessary on 169 | those. 170 | 171 | --ramdisk-load-address ADDR 172 | Add a **load** property to the FIT ramdisk subimage section. The 173 | oldest ARM Chromebooks use an old custom U-Boot that implements the 174 | same verified boot flow as Depthcharge. Its FIT ramdisk support 175 | requires an explicit load address for the ramdisk, which can be 176 | provided with this argument. 177 | 178 | zImage image options 179 | -------------------- 180 | 181 | --pad-vmlinuz, --no-pad-vmlinuz 182 | Pad the *VMLINUZ* file so that the kernel's self-decompression has 183 | enough space to avoid overwriting the *INITRAMFS* file during boot. 184 | The necessary padding is calculated based on values in the zImage 185 | header and the **--kernel-start** argument. 186 | 187 | If the *VMLINUZ* and *INITRAMFS* are small enough (about 16 MiB in 188 | total) they may fit between **--kernel-start** and the start of the 189 | decompression buffer. In this case the padding is unnecessary and 190 | not added. 191 | 192 | The padding is usually larger than the decompressed version of the 193 | kernel, so it results in unbootable images for older boards with 194 | small image size limits. For these, it is usually necessary to use 195 | **--set-init-size**, or custom kernels to make the parts fit as 196 | described above. 197 | 198 | This is disabled by default in favour of **--set-init-size**, use 199 | the **--pad-vmlinuz** argument to enable it. 200 | 201 | --set-init-size, --no-set-init-size 202 | Increase the **init_size** kernel boot parameter so that the 203 | kernel's self-decompression does not overwrite the *INITRAMFS* file 204 | during boot. The modified value is calculated based on values in the 205 | zImage header and the **--kernel-start** argument. 206 | 207 | This only works if the kernel has **KASLR** enabled (as is the 208 | default), because then the kernel itself tries to avoid overwriting 209 | the *INITRAMFS* during decompression. However it does not do this 210 | when first copying the *VMLINUZ* to the end of the decompression 211 | buffer. Increasing **init_size** shifts copy this upwards to avoid 212 | it overlapping *INITRAMFS*. 213 | 214 | If the *VMLINUZ* and *INITRAMFS* are small enough, they may fit 215 | before the first compressed copy's start. In this case changing the 216 | value is unnecessary and skipped. 217 | 218 | This is enabled by default, use the **--no-set-init-size** argument to 219 | disable it. 220 | 221 | Depthcharge image options 222 | ------------------------- 223 | --bootloader FILE 224 | Bootloader stub for the very first Chromebooks that use H2C as their 225 | firmware. Beyond those, this field is ignored on the firmware side 226 | except as a ramdisk for the **multiboot** and **zbi** formats. 227 | 228 | If an *INITRAMFS* is given for the **zimage** format, it is placed 229 | here as part of an initramfs support hack for x86 boards. Otherwise, 230 | an empty file is used. 231 | 232 | -c *CMD* [*CMD* ...], --cmdline *CMD* [*CMD* ...] 233 | Command-line parameters for the kernel. Can be used multiple times 234 | to append new values. If not given, **--** is used. 235 | 236 | The ChromeOS bootloader expands any instance of **%U** in the kernel 237 | command line with the PARTUUID of the ChromeOS kernel partition it 238 | has chosen to boot, e.g. **root=PARTUUID=%U/PARTNROFF=1** will set 239 | the root partition to the one after the booted partition. 240 | 241 | As knowing the currently booted partition is generally useful, 242 | **mkdepthcharge** prepends **kern_guid=%U** to the given kernel 243 | command line parameters to capture it. Use **--no-kern-guid** to 244 | disable this. 245 | 246 | --kern-guid, --no-kern-guid 247 | Prepend **kern_guid=%U** to kernel command-line parameters. This is 248 | enabled by default, use the **--no-kern-guid** argument to disable 249 | it. 250 | 251 | --keydir KEYDIR 252 | Directory containing verified boot keys to use. Equivalent to using 253 | **--keyblock** *KEYDIR*\/|VBOOT_KEYBLOCK|, **--signprivate** 254 | *KEYDIR*\/|VBOOT_SIGNPRIVATE|, and **--signpubkey** *KEYDIR*\ 255 | /|VBOOT_SIGNPUBKEY|. 256 | 257 | --keyblock FILE, --signprivate FILE, --signpubkey FILE 258 | ChromiumOS verified boot keys. More specifically: kernel key block, 259 | private keys in .vbprivk format, and public keys in .vbpubk format. 260 | 261 | If not given, defaults to files set in **depthcharge-tools** 262 | configuration. If those are not set, **mkdepthcharge** searches for 263 | these keys in |CONFIG_DIR| and |VBOOT_DEVKEYS| directories, the 264 | latter being test keys that may be distributed with |vbutil_kernel|. 265 | 266 | You can set these in **depthcharge-tools** configuration by the 267 | **vboot-keyblock**, **vboot-private-key** and **vboot-public-key** 268 | options under a **depthcharge-tools** config section. 269 | 270 | 271 | EXIT STATUS 272 | =========== 273 | In general, exits with zero on success and non-zero on failure. 274 | 275 | 276 | FILES 277 | ===== 278 | |CONFIG_FILE|, |CONFIGD_DIR|/*\ ** 279 | The **depthcharge-tools** configuration files. These might be used 280 | to specify locations of the ChromiumOS verified boot keys as system 281 | configuration. 282 | 283 | |CONFIG_DIR| 284 | The **depthcharge-tools** configuration directory. **mkdepthcharge** 285 | searches this directory for verified boot keys. 286 | 287 | |VBOOT_DEVKEYS| 288 | A directory containing test keys which should have been installed by 289 | |vbutil_kernel|. 290 | 291 | *KEYDIR*/|VBOOT_KEYBLOCK| 292 | Default kernel key block file used for signing the image. 293 | 294 | *KEYDIR*/|VBOOT_SIGNPUBKEY| 295 | Default public key used to verify signed images. 296 | 297 | *KEYDIR*/|VBOOT_SIGNPRIVATE| 298 | Default private key used for signing the image. 299 | 300 | 301 | EXAMPLES 302 | ======== 303 | **mkdepthcharge** **-o** *depthcharge.img* */boot/vmlinuz* 304 | The simplest invocation possible. If tried on an ARM board, the 305 | firmware might refuse to boot the output image since it doesn't have 306 | a dtb for the board. Otherwise, even if the firmware runs the 307 | */boot/vmlinuz* binary, it might not correctly boot due to 308 | non-firmware causes (e.g. kernel panic due to not having a root). 309 | 310 | **mkdepthcharge** **-o** *system.img* **--cmdline** *"root=/dev/mmcblk0p2"* **--compress** *lz4* **--** */boot/vmlinuz.gz* */boot/initrd.img* *rk3399-gru-kevin.dtb* 311 | A command someone using a Samsung Chromebook Plus (v1) might run on 312 | their board to create a bootable image for their running system. 313 | 314 | **mkdepthcharge** **-o** *veyron.img* **-c** *"root=LABEL=ROOT gpt"* **--kernel-start** *0x2000000* **--patch-dtbs** **--** */boot/vmlinuz* */boot/initramfs-linux.img* */boot/dtbs/rk3288-veyron-\*.dtb* 315 | Build an image intended to work on veyron boards like ASUS 316 | Chromebook C201PA and Chromebook Flip C100PA. The stock Depthcharge 317 | on these boards doesn't process the FIT ramdisk, so the dtbs needs 318 | to be patched to boot with initramfs. 319 | 320 | **mkdepthcharge** **-o** *peach-pit.img* **-c** *"console=null"* **--ramdisk-load-address** *0x44000000* **--** *vmlinuz* *initramfs* *exynos5420-peach-pit.dtb* *exynos5420-peach-pit.dtb* 321 | Build an image intended to work on a Samsung Chromebook 2 (11"). 322 | This board uses a custom U-Boot, so needs an explicit ramdisk load 323 | address. Its firmware has a bug with loading the device-tree file, 324 | so needs the file twice for the result to be actually bootable. 325 | 326 | SEE ALSO 327 | ======== 328 | |depthchargectl|, |mkimage|, |vbutil_kernel|, |futility| 329 | 330 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | # depthcharge-tools python package setup script 4 | # Copyright (C) 2020-2023 Alper Nebi Yasak 5 | # See COPYRIGHT and LICENSE files for full copyright information. 6 | 7 | #! /usr/bin/env python3 8 | 9 | import pathlib 10 | import setuptools 11 | 12 | root = pathlib.Path(__file__).resolve().parent 13 | readme = (root / 'README.rst').read_text() 14 | 15 | setuptools.setup( 16 | name='depthcharge-tools', 17 | version='0.6.2', 18 | description='Tools to manage the Chrome OS bootloader', 19 | long_description=readme, 20 | long_description_content_type="text/x-rst", 21 | url='https://github.com/alpernebbi/depthcharge-tools', 22 | author='Alper Nebi Yasak', 23 | author_email='alpernebiyasak@gmail.com', 24 | license='GPL2+', 25 | license_files=["LICENSE", "COPYRIGHT"], 26 | classifiers=[ 27 | 'Development Status :: 3 - Alpha', 28 | 'Environment :: Console', 29 | 'Intended Audience :: Developers', 30 | 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)', 31 | 'Operating System :: POSIX :: Linux', 32 | 'Programming Language :: Python :: 3', 33 | 'Topic :: System :: Boot', 34 | ], 35 | entry_points={ 36 | 'console_scripts': [ 37 | 'mkdepthcharge=depthcharge_tools.mkdepthcharge:mkdepthcharge.main', 38 | 'depthchargectl=depthcharge_tools.depthchargectl:depthchargectl.main', 39 | ], 40 | }, 41 | keywords='ChromeOS ChromiumOS depthcharge vboot vbutil_kernel', 42 | packages=setuptools.find_packages(), 43 | package_data={ 44 | "depthcharge_tools": ["config.ini", "boards.ini"], 45 | }, 46 | install_requires=[ 47 | 'setuptools', 48 | ], 49 | ) 50 | -------------------------------------------------------------------------------- /systemd/90-depthcharge-tools.install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: LGPL-2.1-or-later 3 | 4 | # depthcharge-tools kernel-install plugin 5 | # Copyright (C) 2022 Alper Nebi Yasak 6 | # See COPYRIGHT and LICENSE files for full copyright information. 7 | 8 | # This is a modified copy of 90-loaderentry.install from systemd. 9 | # 10 | # systemd is free software; you can redistribute it and/or modify it 11 | # under the terms of the GNU Lesser General Public License as published by 12 | # the Free Software Foundation; either version 2.1 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # systemd is distributed in the hope that it will be useful, but 16 | # WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 18 | # General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU Lesser General Public License 21 | # along with systemd; If not, see . 22 | 23 | set -e 24 | 25 | COMMAND="${1:?}" 26 | KERNEL_VERSION="${2:?}" 27 | ENTRY_DIR_ABS="${3:?}" 28 | KERNEL_IMAGE="$4" 29 | INITRD_OPTIONS_SHIFT=4 30 | 31 | [ "$KERNEL_INSTALL_LAYOUT" = "depthcharge-tools" ] || exit 0 32 | 33 | log() { 34 | if [ "$KERNEL_INSTALL_VERBOSE" -gt 0 ]; then 35 | echo "$@" 36 | fi 37 | } >&2 38 | 39 | maybe_error() { 40 | # Ignore errors if we're not booted with depthcharge 41 | if grep "cros_secure" /proc/cmdline >/dev/null 2>&1; then 42 | if [ -n "$1" ]; then 43 | echo "Error: $1" 44 | fi 45 | echo "Error: Failed to update depthcharge partitions, system may be unbootable." 46 | exit 1 47 | else 48 | if [ -n "$1" ]; then 49 | log "Error: $1" 50 | fi 51 | log "Not booted with depthcharge, so ignoring that." 52 | exit 0 53 | fi 54 | } >&2 55 | 56 | # Disable if our package is not installed. 57 | if ! command -v depthchargectl >/dev/null 2>&1; then 58 | log "Not running depthcharge plugin, depthchargectl is missing." 59 | exit 0 60 | fi 61 | 62 | run_depthchargectl() { 63 | if [ "$KERNEL_INSTALL_VERBOSE" -gt 0 ]; then 64 | log "Running depthchargectl $@:" 65 | depthchargectl --verbose "$@" 66 | else 67 | depthchargectl "$@" 2>/dev/null 68 | fi 69 | } 70 | 71 | MACHINE_ID="$KERNEL_INSTALL_MACHINE_ID" 72 | ENTRY_TOKEN="$KERNEL_INSTALL_ENTRY_TOKEN" 73 | BOOT_ROOT="$KERNEL_INSTALL_BOOT_ROOT" 74 | 75 | BOOT_MNT="$(stat -c %m "$BOOT_ROOT")" 76 | if [ "$BOOT_MNT" = '/' ]; then 77 | BOOT_DIR="$ENTRY_DIR_ABS" 78 | else 79 | BOOT_DIR="${ENTRY_DIR_ABS#"$BOOT_MNT"}" 80 | fi 81 | 82 | case "$COMMAND" in 83 | remove) 84 | ENABLED="$( 85 | depthchargectl config \ 86 | --section depthchargectl/remove \ 87 | --default False \ 88 | enable-system-hooks 2>/dev/null 89 | )" || maybe_error 90 | 91 | # Disable based on package configuration 92 | if [ "$ENABLED" != "True" ]; then 93 | log "Not removing depthcharge image, disabled by config." 94 | exit 0 95 | fi 96 | 97 | IMAGES_DIR="$( 98 | depthchargectl config \ 99 | --section depthchargectl/remove \ 100 | images-dir 2>/dev/null 101 | )" || maybe_error 102 | 103 | if [ -f "$IMAGES_DIR/$KERNEL_VERSION.img" ]; then 104 | # Assuming kernel-install handles warnings about removing the running kernel 105 | run_depthchargectl remove --force "$KERNEL_VERSION" >/dev/null \ 106 | || maybe_error 107 | else 108 | log "Not removing depthcharge image, already doesn't exist." 109 | fi 110 | 111 | exit 0 112 | ;; 113 | add) 114 | ;; 115 | *) 116 | exit 0 117 | ;; 118 | esac 119 | 120 | ENABLED="$( 121 | depthchargectl config \ 122 | --section depthchargectl/write \ 123 | --default False \ 124 | enable-system-hooks 125 | )" || maybe_error 126 | 127 | if [ "$ENABLED" != "True" ]; then 128 | log "Not writing depthcharge image, disabled by config." 129 | exit 0 130 | fi 131 | 132 | IMAGES_DIR="$( 133 | depthchargectl config \ 134 | --section depthchargectl/write \ 135 | images-dir 136 | )" || maybe_error 137 | 138 | BOARD="$(depthchargectl config board)" || maybe_error 139 | if [ "$BOARD" = "none" ]; then 140 | maybe_error "Cannot build depthcharge images when no board is specified." 141 | fi 142 | 143 | KERNEL_CMDLINE="$( 144 | depthchargectl config \ 145 | --section depthchargectl/write \ 146 | kernel-cmdline 2>/dev/null 147 | )" || KERNEL_CMDLINE="" 148 | 149 | if [ -n "$KERNEL_INSTALL_CONF_ROOT" ]; then 150 | if [ -f "$KERNEL_INSTALL_CONF_ROOT/cmdline" ]; then 151 | BOOT_OPTIONS="$(tr -s "$IFS" ' ' <"$KERNEL_INSTALL_CONF_ROOT/cmdline")" 152 | fi 153 | elif [ -f /etc/kernel/cmdline ]; then 154 | BOOT_OPTIONS="$(tr -s "$IFS" ' ' "$KERNEL_INSTALL_STAGING_AREA/merged-initrd.img" \ 224 | || maybe_error "Could not merge initrd files for depthchargectl." 225 | 226 | INITRD="$KERNEL_INSTALL_STAGING_AREA/merged-initrd.img" 227 | set -- 228 | fi 229 | 230 | # Check possible dtbs paths 231 | FDTDIR="" 232 | for fdtdir in \ 233 | "$BOOT_ROOT/dtbs/$KERNEL_VERSION" \ 234 | "$BOOT_ROOT/dtb/$KERNEL_VERSION" \ 235 | "$BOOT_ROOT/dtbs-$KERNEL_VERSION" \ 236 | "$BOOT_ROOT/dtb-$KERNEL_VERSION" \ 237 | "/usr/lib/linux-image-$KERNEL_VERSION" \ 238 | "/usr/lib/modules/$KERNEL_VERSION/dtbs" \ 239 | "/usr/lib/modules/$KERNEL_VERSION/dtb" \ 240 | "/lib/modules/$KERNEL_VERSION/dtbs" \ 241 | "/lib/modules/$KERNEL_VERSION/dtb" \ 242 | "$BOOT_ROOT/dtbs" \ 243 | "$BOOT_ROOT/dtb" \ 244 | "/usr/share/dtbs" \ 245 | "/usr/share/dtb" \ 246 | ; 247 | do 248 | if [ -d "$fdtdir" ]; then 249 | FDTDIR="$fdtdir" 250 | break 251 | fi 252 | done 253 | 254 | # Depthchargectl write doesn't take custom files, so build image first 255 | IMAGE="$( 256 | run_depthchargectl build \ 257 | --kernel "$KERNEL_IMAGE" \ 258 | ${INITRD:+--initramfs "$INITRD"} \ 259 | ${FDTDIR:+--fdtdir "$FDTDIR"} \ 260 | --kernel-cmdline "$BOOT_OPTIONS" \ 261 | --kernel-release "$KERNEL_VERSION" \ 262 | )" || maybe_error 263 | 264 | PART_COUNT="$(depthchargectl list -c 2>/dev/null)" || maybe_error 265 | if [ "$PART_COUNT" -gt 1 ]; then 266 | run_depthchargectl write "$IMAGE" >/dev/null \ 267 | || maybe_error 268 | 269 | elif [ "$PART_COUNT" -eq 1 ]; then 270 | run_depthchargectl write --allow-current "$IMAGE" >/dev/null \ 271 | || maybe_error 272 | 273 | else 274 | maybe_error "No usable Chrome OS Kernel partition found." 275 | 276 | fi 277 | 278 | exit 0 279 | -------------------------------------------------------------------------------- /systemd/depthchargectl-bless.service: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-2.1+ 2 | 3 | # depthcharge-tools depthchargectl-bless systemd service 4 | # Copyright (C) 2019-2021 Alper Nebi Yasak 5 | # See COPYRIGHT and LICENSE files for full copyright information. 6 | 7 | # This is a modified copy of systemd-bless-boot.service from systemd. 8 | # 9 | # systemd is free software; you can redistribute it and/or modify it 10 | # under the terms of the GNU Lesser General Public License as published by 11 | # the Free Software Foundation; either version 2.1 of the License, or 12 | # (at your option) any later version. 13 | 14 | [Unit] 15 | Description=Mark the current depthcharge partition as successful 16 | Documentation=man:depthchargectl(8) 17 | DefaultDependencies=no 18 | Requires=boot-complete.target 19 | After=local-fs.target boot-complete.target 20 | Conflicts=shutdown.target 21 | Before=shutdown.target 22 | ConditionKernelCommandLine=cros_secure 23 | 24 | [Service] 25 | Type=oneshot 26 | RemainAfterExit=yes 27 | ExecStart=depthchargectl bless 28 | 29 | # systemd-bless-boot-generator symlinks its file to basic.target.wants. 30 | [Install] 31 | WantedBy=basic.target 32 | --------------------------------------------------------------------------------