pax_global_header00006660000000000000000000000064137275174640014532gustar00rootroot0000000000000052 comment=be2f596522225a36239fdd666dedfbf57b5ad378 pibootctl-0.5.2/000077500000000000000000000000001372751746400135355ustar00rootroot00000000000000pibootctl-0.5.2/.gitignore000066400000000000000000000003301372751746400155210ustar00rootroot00000000000000# Python stuff *.py[cdo] # Vim stuff *.vim *.swp # Miscellaneous tags # Packages *.egg *.egg-info *.pyc *.whl dist build man # Unit test / coverage reports coverage .cache .coverage .tox .pytest_cache .env .eggs pibootctl-0.5.2/LICENSE.txt000066400000000000000000001045161372751746400153670ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . pibootctl-0.5.2/Makefile000066400000000000000000000136541372751746400152060ustar00rootroot00000000000000# vim: set noet sw=4 ts=4 fileencoding=utf-8: # External utilities PYTHON=python3 PIP=pip PYTEST=pytest COVERAGE=coverage TWINE=twine PYFLAGS= DEST_DIR=/ # Calculate the base names of the distribution, the location of all source, # documentation, packaging, icon, and executable script files NAME:=$(shell $(PYTHON) $(PYFLAGS) setup.py --name) PKG_DIR:=$(subst -,_,$(NAME)) VER:=$(shell $(PYTHON) $(PYFLAGS) setup.py --version) DEB_ARCH:=$(shell dpkg --print-architecture) DEB_SUFFIX:= PY_SOURCES:=$(shell \ $(PYTHON) $(PYFLAGS) setup.py egg_info >/dev/null 2>&1 && \ grep -v "\.egg-info" $(PKG_DIR).egg-info/SOURCES.txt) DEB_SOURCES:=debian/changelog \ debian/control \ debian/copyright \ debian/rules \ debian/docs \ $(wildcard debian/*.init) \ $(wildcard debian/*.default) \ $(wildcard debian/*.manpages) \ $(wildcard debian/*.docs) \ $(wildcard debian/*.doc-base) \ $(wildcard debian/*.desktop) DOC_SOURCES:=docs/conf.py \ $(wildcard docs/*.png) \ $(wildcard docs/*.svg) \ $(wildcard docs/*.dot) \ $(wildcard docs/*.mscgen) \ $(wildcard docs/*.gpi) \ $(wildcard docs/*.rst) \ $(wildcard docs/*.pdf) SUBDIRS:= # Calculate the name of all outputs DIST_WHEEL=dist/$(NAME)-$(VER)-py3-none-any.whl DIST_TAR=dist/$(NAME)-$(VER).tar.gz DIST_ZIP=dist/$(NAME)-$(VER).zip DIST_DEB=dist/$(NAME)-master_$(VER)$(DEB_SUFFIX)_all.deb \ dist/$(NAME)-slave_$(VER)$(DEB_SUFFIX)_all.deb \ dist/$(NAME)-docs_$(VER)$(DEB_SUFFIX)_all.deb \ dist/$(NAME)_$(VER)$(DEB_SUFFIX)_$(DEB_ARCH).build \ dist/$(NAME)_$(VER)$(DEB_SUFFIX)_$(DEB_ARCH).buildinfo \ dist/$(NAME)_$(VER)$(DEB_SUFFIX)_$(DEB_ARCH).changes DIST_DSC=dist/$(NAME)_$(VER)$(DEB_SUFFIX).tar.xz \ dist/$(NAME)_$(VER)$(DEB_SUFFIX).dsc \ dist/$(NAME)_$(VER)$(DEB_SUFFIX)_source.build \ dist/$(NAME)_$(VER)$(DEB_SUFFIX)_source.buildinfo \ dist/$(NAME)_$(VER)$(DEB_SUFFIX)_source.changes MAN_PAGES=man/pibootctl.1 \ man/pibootctl-help.1 \ man/pibootctl-status.1 \ man/pibootctl-get.1 \ man/pibootctl-set.1 \ man/pibootctl-save.1 \ man/pibootctl-load.1 \ man/pibootctl-diff.1 \ man/pibootctl-show.1 \ man/pibootctl-list.1 \ man/pibootctl-remove.1 \ man/pibootctl-rename.1 # Default target all: @echo "make install - Install on local system" @echo "make develop - Install symlinks for development" @echo "make test - Run tests" @echo "make doc - Generate HTML and PDF documentation" @echo "make source - Create source package" @echo "make wheel - Generate a PyPI wheel package" @echo "make zip - Generate a source zip package" @echo "make tar - Generate a source tar package" @echo "make deb - Generate Debian packages" @echo "make dist - Generate all packages" @echo "make clean - Get rid of all generated files" @echo "make release - Create and tag a new release" @echo "make upload - Upload the new release to repositories" install: $(SUBDIRS) $(PYTHON) $(PYFLAGS) setup.py install --root $(DEST_DIR) doc: $(DOC_SOURCES) $(MAKE) -C docs clean $(MAKE) -C docs html $(MAKE) -C docs epub $(MAKE) -C docs latexpdf $(MAKE) $(MAN_PAGES) source: $(DIST_TAR) $(DIST_ZIP) wheel: $(DIST_WHEEL) zip: $(DIST_ZIP) tar: $(DIST_TAR) deb: $(DIST_DEB) $(DIST_DSC) dist: $(DIST_WHEEL) $(DIST_DEB) $(DIST_DSC) $(DIST_TAR) $(DIST_ZIP) develop: tags @# These have to be done separately to avoid a cockup... $(PIP) install -U setuptools $(PIP) install -U pip $(PIP) install tox $(PIP) install -e .[doc,test] test: $(PYTHON) -m $(PYTEST) clean: -[ -d debian ] && dh_clean rm -fr build/ dist/ man/ $(NAME).egg-info/ tags for dir in $(SUBDIRS); do \ $(MAKE) -C $$dir clean; \ done find $(CURDIR) -name "*.pyc" -delete find $(CURDIR) -name "__pycache__" -delete tags: $(PY_SOURCES) ctags -R --exclude="build/*" --exclude="debian/*" --exclude="docs/*" --languages="Python" lint: $(PY_SOURCES) pylint piwheels $(SUBDIRS): $(MAKE) -C $@ $(MAN_PAGES): $(DOC_SOURCES) $(PYTHON) $(PYFLAGS) setup.py build_sphinx -b man mkdir -p man/ cp build/sphinx/man/*.[0-9] man/ $(DIST_TAR): $(PY_SOURCES) $(SUBDIRS) $(PYTHON) $(PYFLAGS) setup.py sdist --formats gztar $(DIST_ZIP): $(PY_SOURCES) $(SUBDIRS) $(PYTHON) $(PYFLAGS) setup.py sdist --formats zip $(DIST_WHEEL): $(PY_SOURCES) $(SUBDIRS) $(PYTHON) $(PYFLAGS) setup.py bdist_wheel $(DIST_DEB): $(PY_SOURCES) $(SUBDIRS) $(DEB_SOURCES) $(MAN_PAGES) # build the binary package in the parent directory then rename it to # project_version.orig.tar.gz $(PYTHON) $(PYFLAGS) setup.py sdist --dist-dir=../ rename -f 's/$(NAME)-(.*)\.tar\.gz/$(NAME)_$$1\.orig\.tar\.gz/' ../* debuild -b mkdir -p dist/ for f in $(DIST_DEB); do cp ../$${f##*/} dist/; done $(DIST_DSC): $(PY_SOURCES) $(SUBDIRS) $(DEB_SOURCES) $(MAN_PAGES) # build the source package in the parent directory then rename it to # project_version.orig.tar.gz $(PYTHON) $(PYFLAGS) setup.py sdist --dist-dir=../ rename -f 's/$(NAME)-(.*)\.tar\.gz/$(NAME)_$$1\.orig\.tar\.gz/' ../* debuild -S mkdir -p dist/ for f in $(DIST_DSC); do cp ../$${f##*/} dist/; done changelog: $(PY_SOURCES) $(DOC_SOURCES) $(DEB_SOURCES) $(MAKE) clean # ensure there are no current uncommitted changes test -z "$(shell git status --porcelain)" # update the debian changelog with new release information dch --newversion $(VER)$(DEB_SUFFIX) # commit the changes and add a new tag git commit debian/changelog -m "Updated changelog for release $(VER)" release-pi: $(PY_SOURCES) $(DOC_SOURCES) $(DIST_DEB) $(DIST_DSC) git tag -s release-$(VER) -m "Release $(VER)" git push --tags git push # build a source archive and upload to PyPI $(TWINE) upload $(DIST_TAR) $(DIST_WHEEL) # build the deb source archive and upload to Raspbian dput raspberrypi dist/$(NAME)_$(VER)$(DEB_SUFFIX)_source.changes dput raspberrypi dist/$(NAME)_$(VER)$(DEB_SUFFIX)_$(DEB_ARCH).changes release-ubuntu: $(DIST_DEB) $(DIST_DSC) # build the deb source archive and upload to the PPA dput waveform-ppa dist/$(NAME)_$(VER)$(DEB_SUFFIX)_source.changes .PHONY: all install develop test doc source wheel zip tar deb dist clean tags changelog release-pi release-ubuntu $(SUBDIRS) pibootctl-0.5.2/README.rst000066400000000000000000000041101372751746400152200ustar00rootroot00000000000000========= pibootctl ========= pibootctl is a utility for querying and manipulating the boot configuration of a `Raspberry Pi`_. It is a relatively low level utility, and not intended to be as friendly (or as widely applicable) as ``raspi-config``. It provides a command line interface only, but does attempt to be useful as a basis for more advanced interfaces (by providing input and output in human-readable, shell-interpretable, JSON, or YAML formats) as well as being useful in its own right. The design philosophy of the utility is as follows: #. Be safe: the utility manipulates the boot configuration and it's entirely possible to create a non-booting system as a result. To that end, if no backup of the current boot configuration exists, always take one before manipulating it. #. Be accessible: the Pi's boot configuration lives on a FAT partition and is a simple ASCII text file. This means it can be read and manipulated by almost any platform (Windows, Mac OS, etc). Any backups of the configuration should be as accessible. To that end we use simple PKZIP files to store backups of boot configurations (in their original format), and place them on the same FAT partition as the configuration. #. Be extensible: Almost all commands should default to human readable input and output, but options should be provided for I/O in JSON, YAML, and a shell-parseable format. Links ===== * The code is licensed under the `GPL v3`_ or above * The `source code`_ can be obtained from GitHub, which also hosts the `bug tracker`_ * The `documentation`_ (which includes installation and quick start examples) can be read on ReadTheDocs * Packages can be `downloaded`_ from PyPI, although reading the installation instructions will probably be more useful .. _Raspberry Pi: https://raspberrypi.org/ .. _GPL v3: https://www.gnu.org/licenses/gpl-3.0.html .. _source code: https://github.com/waveform80/pibootctl .. _bug tracker: https://github.com/waveform80/pibootctl/issues .. _documentation: https://pibootctl.readthedocs.io/ .. _downloaded: https://pypi.org/project/pibootctl pibootctl-0.5.2/copyrights000077500000000000000000000217361372751746400156670ustar00rootroot00000000000000#!/usr/bin/env python """ This is a small utility script I originally whipped up for dealing with re-writing the copyright headers in GPIO-Zero which had so many myriad contributors it was becoming a pain to keep the headers accurate. Usage is simple: just run ./copyrights in a clone of the repo after all PRs have been merged and just before you're about to do a release. It should (if it's working properly!) rewrite the copyright headers in all the files. If you want to change its operation, the crucial bits are get_license() which dictates what legalese gets written at the top, and update_copyright() which does the actual re-writing (and which can be adapted to insert, e.g. a corporate copyright holder before/after all individual contributors). Everything else gets derived from the git history, so as long as this is reasonable the output should be too. Obviously, you should check the "git diff" is vaguely sane before committing it! """ import io import re import sys from collections import namedtuple from operator import attrgetter from itertools import groupby, tee from datetime import datetime from subprocess import Popen, PIPE, DEVNULL from pathlib import Path from fnmatch import fnmatch from functools import lru_cache Contribution = namedtuple('Contribution', ('author', 'email', 'year', 'filename')) class Copyright(namedtuple('Copyright', ('author', 'email', 'years'))): def __str__(self): if self.email: name = '{self.author} <{self.email}>'.format(self=self) else: name = self.author return 'Copyright (c) {years} {name}'.format( years=int_ranges(self.years), name=name) def main(): includes = { '**/*.py', '**/*.rst', } excludes = { 'docs/examples/*.py', 'docs/license.rst', } prefixes = { '.py': '#', '.rst': '..', } if len(sys.argv) > 1: includes = set(sys.argv[1:]) contributions = get_contributions(includes, excludes) for filename, copyrights in contributions.items(): filename = Path(filename) update_copyright(filename, copyrights, prefixes[filename.suffix]) def get_contributions(include, exclude): sorted_blame = sorted( get_blame(include, exclude), key=lambda c: (c.filename, c.author, c.email) ) blame_by_file = { filename: list(file_contributions) for filename, file_contributions in groupby( sorted_blame, key=attrgetter('filename') ) } return { filename: { Copyright(author, email, frozenset(y.year for y in years)) for (author, email), years in groupby( file_contributors, key=lambda c: (c.author, c.email) ) } for filename, file_contributors in blame_by_file.items() } def get_blame(include, exclude): for filename in get_source_files(include, exclude): blame = Popen( ['git', 'blame', '--line-porcelain', 'HEAD', '--', filename], stdout=PIPE, stderr=PIPE, universal_newlines=True ) author = email = year = None for line in blame.stdout: if line.startswith('author '): author = line.split(' ', 1)[1].rstrip() elif line.startswith('author-mail '): email = line.split(' ', 1)[1].rstrip().lstrip('<').rstrip('>') elif line.startswith('author-time '): # Forget the timezone; we only want the year anyway year = datetime.fromtimestamp(int(line.split(' ', 1)[1].rstrip())).year elif line.startswith('filename '): yield Contribution( author=author, email=email, year=year, filename=filename) author = email = year = None blame.wait() assert blame.returncode == 0 def get_source_files(include, exclude): ls_tree = Popen( ['git', 'ls-tree', '-r', '--name-only', 'HEAD'], stdout=PIPE, stderr=DEVNULL, universal_newlines=True ) if not include: include = {'*'} for filename in ls_tree.stdout: filename = filename.strip() if any(fnmatch(filename, pattern) for pattern in exclude): continue if any(fnmatch(filename, pattern) for pattern in include): yield filename ls_tree.wait() assert ls_tree.returncode == 0 insertion_point = object() def parse_source_file(filename, prefix): license = get_license() license_start = license[0] license_end = license[-1] with filename.open('r') as source: state = 'preamble' for linenum, line in enumerate(source, start=1): if state == 'preamble': if linenum == 1 and line.startswith('#!'): yield line elif linenum < 10 and 'set fileencoding' in line: yield line elif line.rstrip() == prefix: pass # skip blank comment lines elif line.startswith(prefix + ' Copyright (c)'): pass # skip existing copyright lines elif line.startswith(prefix + ' ' + license_start): state = 'license' # skip existing license lines else: yield insertion_point state = 'blank' elif state == 'license': if line.startswith(prefix + ' ' + license_end): yield insertion_point state = 'blank' continue if state == 'blank': # Ensure there's a blank line between license and start of the # source body if line.strip(): yield '\n' yield line state = 'body' elif state == 'body': yield line def update_copyright(filename, copyrights, prefix): print('Re-writing {filename}...'.format(filename=filename)) license = get_license() copyrights = [ Copyright('Canonical Ltd.', '', {datetime.now().year}), ] + sorted( copyrights, reverse=True, key=lambda c: (c.years[::-1] if isinstance(c.years, tuple) else (c.years,), c.author) ) content = [] for line in parse_source_file(filename, prefix): if line is insertion_point: if len(content) > 0: content.append(prefix + '\n') for copyright in copyrights: content.append(prefix + ' ' + str(copyright) + '\n') content.append(prefix + '\n') content.extend( (prefix + ' ' + l).strip() + '\n' for l in license ) else: content.append(line) # Yes, if I was doing this "properly" I'd write to a temporary file and # rename it over the target. However, I'm assuming you're running this # under a git clone ... after all, you are ... aren't you? with filename.open('w') as target: for line in content: target.write(line) @lru_cache() def get_license(): return """\ This file is part of pibootctl. pibootctl is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. pibootctl is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with pibootctl. If not, see . """.splitlines() def pairwise(iterable): """ Taken from the recipe in the documentation for :mod:`itertools`. """ a, b = tee(iterable) next(b, None) return zip(a, b) def int_ranges(values): """ Given a set of integer *values*, returns a compressed string representation of all values in the set. For example: >>> int_ranges({1, 2}) '1, 2' >>> int_ranges({1, 2, 3}) '1-3' >>> int_ranges({1, 2, 3, 4, 8}) '1-4, 8' >>> int_ranges({1, 2, 3, 4, 8, 9}) '1-4, 8-9' """ if len(values) == 0: return '' elif len(values) == 1: return '{0}'.format(*values) elif len(values) == 2: return '{0}, {1}'.format(*values) else: ranges = [] start = None for i, j in pairwise(sorted(values)): if start is None: start = i if j > i + 1: ranges.append((start, i)) start = j if j == i + 1: ranges.append((start, j)) else: ranges.append((j, j)) return ', '.join( ('{start}-{finish}' if finish > start else '{start}').format( start=start, finish=finish) for start, finish in ranges ) if __name__ == '__main__': main() pibootctl-0.5.2/design.txt000066400000000000000000000063031372751746400155510ustar00rootroot00000000000000pibootctl set ... pibootctl get pibootctl get --json pibootctl set i2c=on spi=on pibootctl get i2c Example output: $ pibootctl get Setting Value Config ================= ===== ====== x audio.enabled off dtparam=audio bt.enabled off dtoverlay? x i2c.enabled on dtparam=i2c_arm x i2c.baud 100000 dtparam=i2c_arm_baudrate x i2s.enabled off dtparam=i2s w1.enabled off dtoverlay=w1-gpio w1.gpio 4 dtparam=gpiopin=4 x spi.enabled on dtparam=spi uart.enabled on enable_uart camera.enabled off start_x/start x video.cec.enabled on hdmi_ignore_cec x video.cec.init on hdmi_ignore_cec_init x video.cec.osd_name "Raspberry Pi" cec_osd_name x video.hdmi.force_3d off hdmi_force_edid_3d x video.hdmi.blanking off hdmi_blanking video.hdmi.timings auto hdmi_timings video.hdmi.pixel_encoding 0 hdmi_pixel_encoding video.hdmi.boost 5 config_hdmi_boost video.hdmi0.flip none display_hdmi_rotate video.hdmi0.force_mode off hdmi_force_mode video.hdmi0.group auto hdmi_group video.hdmi0.mode auto hdmi_mode video.hdmi0.rotate 0 display_hdmi_rotate video.hdmi1.flip off display_hdmi_rotate:1 video.hdmi1.force_mode off hdmi_force_mode:1 video.hdmi1.group auto hdmi_group:1 video.hdmi1.mode auto hdmi_mode:1 video.hdmi1.rotate 0 display_hdmi_rotate:1 video.dpi.enabled off enable_dpi_lcd video.dpi.group auto dpi_group video.dpi.mode auto dpi_mode video.dpi.timings auto dpi_timings video.lcd.default on display_default_lcd video.lcd.flip off display_lcd_rotate video.lcd.framerate 60 lcd_framerate video.lcd.ignore off ignore_lcd video.lcd.rotate 0 lcd_rotate / display_lcd_rotate video.lcd.touchscreen on disable_touchscreen video.mem auto gpu_mem x video.overscan.enabled off disable_overscan x video.overscan.left 0 overscan_left x video.overscan.right 0 overscan_left x video.overscan.top 0 overscan_top x video.overscan.bottom 0 overscan_bottom x watchdog.enabled off dtparam=watchdog pibootctl-0.5.2/docs/000077500000000000000000000000001372751746400144655ustar00rootroot00000000000000pibootctl-0.5.2/docs/Makefile000066400000000000000000000170361372751746400161340ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = ../build DOT_DIAGRAMS := $(wildcard images/*.dot) MSC_DIAGRAMS := $(wildcard images/*.mscgen) GPI_DIAGRAMS := $(wildcard images/*.gpi) SVG_IMAGES := $(wildcard images/*.svg) $(DOT_DIAGRAMS:%.dot=%.svg) $(MSC_DIAGRAMS:%.mscgen=%.svg) PNG_IMAGES := $(wildcard images/*.png) $(GPI_DIAGRAMS:%.gpi=%.png) $(SVG_IMAGES:%.svg=%.png) PDF_IMAGES := $(SVG_IMAGES:%.svg=%.pdf) $(GPI_DIAGRAMS:%.gpi=%.pdf) $(DOT_DIAGRAMS:%.dot=%.pdf) $(MSC_DIAGRAMS:%.mscgen=%.pdf) # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SVG_IMAGES) $(PNG_IMAGES) $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SVG_IMAGES) $(PNG_IMAGES) $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SVG_IMAGES) $(PNG_IMAGES) $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pibootctl.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pibootctl.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/pibootctl" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pibootctl" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(PDF_IMAGES) $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(PDF_IMAGES) $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(PDF_IMAGES) $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." %.svg: %.mscgen mscgen -T svg -o $@ $< %.svg: %.dot dot -T svg -o $@ $< %.png: %.gpi gnuplot -e "set term pngcairo transparent size 400,400" $< > $@ %.png: %.svg inkscape -e $@ $< %.pdf: %.svg inkscape -A $@ $< %.pdf: %.gpi gnuplot -e "set term pdfcairo size 5cm,5cm" $< > $@ %.pdf: %.mscgen mscgen -T eps -o - $< | ps2pdf -dEPSCrop - $@ .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext pibootctl-0.5.2/docs/_static/000077500000000000000000000000001372751746400161135ustar00rootroot00000000000000pibootctl-0.5.2/docs/_static/style_override.css000066400000000000000000000005161372751746400216660ustar00rootroot00000000000000/* override table width restrictions */ .wy-table-responsive table td, .wy-table-responsive table th { /* !important prevents the common CSS stylesheets from overriding this as on RTD they are loaded after this stylesheet */ white-space: normal !important; } .wy-table-responsive { overflow: visible !important; } pibootctl-0.5.2/docs/api.rst000066400000000000000000000026211372751746400157710ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . === API === :doc:`pibootctl ` can be used both as a standalone application, and as an API within Python. The primary class of interest when using :doc:`pibootctl ` as an API is :class:`~pibootctl.store.Store` in the :mod:`pibootctl.store` module, but :mod:`pibootctl.main` is useful for providing an instance of this, constructed from the pibootctl configuration. The API is split into several modules, documented in the following sections: .. toctree:: :maxdepth: 1 api_exc api_files api_formatter api_info api_main api_parser api_setting api_settings api_store api_term api_userstr pibootctl-0.5.2/docs/api_exc.rst000066400000000000000000000015161372751746400166320ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . ============= pibootctl.exc ============= .. automodule:: pibootctl.exc pibootctl-0.5.2/docs/api_files.rst000066400000000000000000000015261372751746400171560ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . =============== pibootctl.files =============== .. automodule:: pibootctl.files pibootctl-0.5.2/docs/api_formatter.rst000066400000000000000000000015461372751746400200610ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . =================== pibootctl.formatter =================== .. automodule:: pibootctl.formatter pibootctl-0.5.2/docs/api_info.rst000066400000000000000000000015221372751746400170030ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . ============== pibootctl.info ============== .. automodule:: pibootctl.info pibootctl-0.5.2/docs/api_main.rst000066400000000000000000000015221372751746400167740ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . ============== pibootctl.main ============== .. automodule:: pibootctl.main pibootctl-0.5.2/docs/api_parser.rst000066400000000000000000000015321372751746400173450ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . ================ pibootctl.parser ================ .. automodule:: pibootctl.parser pibootctl-0.5.2/docs/api_setting.rst000066400000000000000000000015361372751746400175320ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . ================= pibootctl.setting ================= .. automodule:: pibootctl.setting pibootctl-0.5.2/docs/api_settings.rst000066400000000000000000000015421372751746400177120ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . ================== pibootctl.settings ================== .. automodule:: pibootctl.settings pibootctl-0.5.2/docs/api_store.rst000066400000000000000000000015261372751746400172100ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . =============== pibootctl.store =============== .. automodule:: pibootctl.store pibootctl-0.5.2/docs/api_term.rst000066400000000000000000000015221372751746400170170ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . ============== pibootctl.term ============== .. automodule:: pibootctl.term pibootctl-0.5.2/docs/api_userstr.rst000066400000000000000000000015361372751746400175640ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . ================= pibootctl.userstr ================= .. automodule:: pibootctl.userstr pibootctl-0.5.2/docs/changelog.rst000066400000000000000000000051211372751746400171450ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . ========= Changelog ========= .. currentmodule:: pibootctl Release 0.5.2 (2020-09-14) ========================== * Fix handling of initramfs (ramfsaddr=0 doesn't work) Release 0.5.1 (2020-09-09) ========================== * Handle future model numbers elegantly * Rewrote the configuration setting code to always target :file:`config.txt` as several settings don't work in included files (e.g. ``start_x``). * Added ``comment_lines`` configuration option to permit commenting out lines instead of deleting them * Enhanced the configuration setting code to search for and uncomment existing lines in preference to writing new ones * Added ``--this-model`` and ``--this-serial`` options to permit adding settings in new conditional sections Release 0.4 (2020-03-31) ======================== * Handle unrecognized commands correctly in the "help" command * Implemented loading settings with the ``--shell`` style * Improved help output for reference lists * Fixed all legal stuff (added copyright headers where required, re-licensed to GPL 3+) Release 0.3 (2020-03-27) ======================== * Added full bash completion support Release 0.2 (2020-03-26) ======================== * The application now reports which lines overrode a setting when the "ineffective setting" error occurs * Added the max_framebuffers setting, and detection for the vc4-\*-v3d overlays * Fixed restoring the default configuration in which config.txt doesn't exist (i.e. when config.txt should be deleted or blanked; the prior version simply left the old config.txt in place incorrectly) * Various documentation fixes Release 0.1.1 (2020-03-13) ========================== * Fixed broken build on Bionic Release 0.1 (2020-03-13) ======================== * Initial release. * Please note that as this is a pre-v1 release, API stability is not yet guaranteed. pibootctl-0.5.2/docs/commands.rst000066400000000000000000000031651372751746400170250ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . :doc:`diff` Display the differences between the specified boot configuration and the current one, or another specified configuration. :doc:`get` Retrieve the value of specified setting(s). :doc:`help` The default command, which describes the specified command or configuration setting. :doc:`list` List the stored boot configurations. :doc:`load` Restore the named boot configuration to be used at the next boot. :doc:`remove` Delete the specified boot configuration. :doc:`rename` Rename the specified boot configuration. :doc:`save` Save the current boot configuration to the specified name. :doc:`set` Modify or reset the specified configuration setting(s). :doc:`show` Show the specified stored configuration. :doc:`status` Output the current boot configuration; by default this only prints modified settings. pibootctl-0.5.2/docs/conf.py000066400000000000000000000140341372751746400157660ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . # vim: set et sw=4 sts=4 fileencoding=utf-8: import sys import os import pkginfo from datetime import datetime on_rtd = os.environ.get('READTHEDOCS', None) == 'True' info = pkginfo.Installed('pibootctl') if info.version is None: # pkginfo prior to 1.5 seems broken (succeeds in construction, but doesn't # contain any info); fall back to a horrific configparser hack... import configparser cfg = configparser.ConfigParser() class Attr(dict): __getattr__ = dict.__getitem__ assert cfg.read(('../setup.cfg',)) info = Attr(**cfg['metadata']) if info.version is None: raise RuntimeError('Failed to load distro info') # -- General configuration ------------------------------------------------ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx'] if on_rtd: needs_sphinx = '1.4.0' extensions.append('sphinx.ext.imgmath') imgmath_image_format = 'svg' tags.add('rtd') else: extensions.append('sphinx.ext.mathjax') mathjax_path = '/usr/share/javascript/mathjax/MathJax.js?config=TeX-AMS_HTML' templates_path = ['_templates'] source_suffix = '.rst' #source_encoding = 'utf-8-sig' master_doc = 'index' project = info.name copyright = '2019-%s %s' % (datetime.now().year, info.author) version = info.version #release = None #language = None #today_fmt = '%B %d, %Y' exclude_patterns = ['_build'] highlight_language = 'python3' #default_role = None #add_function_parentheses = True #add_module_names = True #show_authors = False pygments_style = 'sphinx' #modindex_common_prefix = [] #keep_warnings = False # -- Autodoc configuration ------------------------------------------------ autodoc_member_order = 'groupwise' # -- Intersphinx configuration -------------------------------------------- intersphinx_mapping = { 'python': ('https://docs.python.org/3.5', None), } intersphinx_cache_limit = 7 # -- Options for HTML output ---------------------------------------------- if on_rtd: html_theme = 'sphinx_rtd_theme' pygments_style = 'default' #html_theme_options = {} #html_sidebars = {} else: html_theme = 'default' #html_theme_options = {} #html_sidebars = {} html_title = '%s %s Documentation' % (project, version) #html_theme_path = [] #html_short_title = None #html_logo = None #html_favicon = None html_static_path = ['_static'] #html_extra_path = [] #html_last_updated_fmt = '%b %d, %Y' #html_use_smartypants = True #html_additional_pages = {} #html_domain_indices = True #html_use_index = True #html_split_index = False #html_show_sourcelink = True #html_show_sphinx = True #html_show_copyright = True #html_use_opensearch = '' #html_file_suffix = None htmlhelp_basename = '%sdoc' % info.name # Hack to make wide tables work properly in RTD # See https://github.com/snide/sphinx_rtd_theme/issues/117 for details def setup(app): app.add_stylesheet('style_override.css') # -- Options for LaTeX output --------------------------------------------- latex_engine = 'xelatex' latex_elements = { 'papersize': 'a4paper', 'pointsize': '10pt', 'preamble': r'\def\thempfootnote{\arabic{mpfootnote}}', # workaround sphinx issue #2530 } latex_documents = [ ( 'index', # source start file '%s.tex' % project, # target filename '%s %s Documentation' % (project, version), # title info.author, # author 'manual', # documentclass True, # documents ref'd from toctree only ), ] #latex_logo = None #latex_use_parts = False latex_show_pagerefs = True latex_show_urls = 'footnote' #latex_appendices = [] #latex_domain_indices = True # -- Options for epub output ---------------------------------------------- epub_basename = project #epub_theme = 'epub' #epub_title = html_title epub_author = info.author epub_identifier = 'https://pibootctl.readthedocs.io/' #epub_tocdepth = 3 epub_show_urls = 'no' #epub_use_index = True # -- Options for manual page output --------------------------------------- man_pages = [ ('pibootctl', 'pibootctl', 'pibootctl manual', [info.author], 1), ('help', 'pibootctl-help', 'pibootctl manual', [info.author], 1), ('status', 'pibootctl-status', 'pibootctl manual', [info.author], 1), ('get', 'pibootctl-get', 'pibootctl manual', [info.author], 1), ('set', 'pibootctl-set', 'pibootctl manual', [info.author], 1), ('save', 'pibootctl-save', 'pibootctl manual', [info.author], 1), ('load', 'pibootctl-load', 'pibootctl manual', [info.author], 1), ('diff', 'pibootctl-diff', 'pibootctl manual', [info.author], 1), ('show', 'pibootctl-show', 'pibootctl manual', [info.author], 1), ('list', 'pibootctl-list', 'pibootctl manual', [info.author], 1), ('remove', 'pibootctl-remove', 'pibootctl manual', [info.author], 1), ('rename', 'pibootctl-rename', 'pibootctl manual', [info.author], 1), ] man_show_urls = True # -- Options for Texinfo output ------------------------------------------- texinfo_documents = [] #texinfo_appendices = [] #texinfo_domain_indices = True #texinfo_show_urls = 'footnote' #texinfo_no_detailmenu = False # -- Options for linkcheck builder ---------------------------------------- linkcheck_retries = 3 linkcheck_workers = 20 linkcheck_anchors = True pibootctl-0.5.2/docs/development.rst000066400000000000000000000077471372751746400175600ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . .. _dev_install: =========== Development =========== If you wish to install a copy of pibootctl for development purposes, clone the git repository and set up a configuration to use the cloned directory as the source of the boot configuration: .. code-block:: console $ sudo apt install python3-dev git virtualenvwrapper $ cd $ git clone https://github.com/waveform80/pibootctl.git $ mkvirtualenv -p /usr/bin/python3 pibootctl $ cd pibootctl $ workon pibootctl (pibootctl) $ make develop (pibootctl) $ cat > ~/.config/pibootctl.conf << EOF [defaults] boot_path=. store_path=store reboot_required= reboot_required_pkgs= EOF At this point you should be able to call the :doc:`pibootctl ` utility, and have it store the (empty) boot configuration as a `PKZIP`_ file under the working directory: .. code-block:: console $ pibootctl save foo $ pibootctl ls +------+--------+---------------------+ | Name | Active | Timestamp | |------+--------+---------------------| | foo | x | 2020-03-08 22:40:28 | +------+--------+---------------------+ To work on the clone in future simply enter the directory and use the :command:`workon` command: .. code-block:: console $ cd ~/pibootctl $ workon pibootctl To pull the latest changes from git into your clone and update your installation: .. code-block:: console $ cd ~/pibootctl $ workon pibootctl (pibootctl) $ git pull (pibootctl) $ make develop To remove your installation, destroy the sandbox and the clone: .. code-block:: console (pibootctl) $ cd (pibootctl) $ deactivate $ rmvirtualenv pibootctl $ rm -fr ~/pibootctl Building the docs ================= If you wish to build the docs, you'll need a few more dependencies. Inkscape is used for conversion of SVGs to other formats, Graphviz and Gnuplot are used for rendering certain charts, and TeX Live is required for building PDF output. The following command should install all required dependencies: .. code-block:: console $ sudo apt install texlive-xetex fonts-freefont-otf graphviz gnuplot inkscape Once these are installed, you can use the "doc" target to build the documentation: .. code-block:: console $ cd ~/pibootctl $ workon pibootctl (pibootctl) $ make doc The HTML output is written to :file:`build/html` while the PDF output goes to :file:`build/latex`. Test suite ========== If you wish to run the test suite, follow the instructions in :ref:`dev_install` above and then make the "test" target within the sandbox: .. code-block:: console $ cd ~/pibootctl $ workon pibootctl (pibootctl) $ make test A `tox`_ configuration is also provided that will test the utility against all supported Python versions: .. code-block:: console $ cd ~/pibootctl $ workon pibootctl (pibootctl) $ pip install tox ... (pibootctl) $ tox -p auto .. note:: If developing under Ubuntu, the `Dead Snakes PPA`_ is particularly useful for obtaining additional Python installations for testing. .. _PKZIP: https://en.wikipedia.org/wiki/Zip_(file_format) .. _tox: https://tox.readthedocs.io/en/latest/ .. _Dead Snakes PPA: https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa pibootctl-0.5.2/docs/diff.rst000066400000000000000000000061751372751746400161400ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . ==== diff ==== .. program:: pibootctl-diff Synopsis ======== .. code-block:: text pibootctl diff [-h] [--json | --yaml | --shell] [left] right Description =========== Display the settings that differ between two stored boot configurations, or between one stored boot configuration and the current configuration. Options ======= .. option:: left The boot configuration to compare from, or the current configuration if omitted. .. option:: right The boot configuration to compare against. .. option:: -h, --help Show a brief help page for the command. .. option:: --json Use JSON as the output format. .. option:: --yaml Use YAML as the output format. .. option:: --shell Use a tab-delimited output format suitable for the shell. Usage ===== The :command:`diff` command is used to display the differences between two boot configurations; either two stored configurations (if two names are supplied on the command line), or between the current boot configuration and a stored one (if one name is supplied on the command line): .. code-block:: console $ sudo pibootctl save default $ sudo pibootctl set video.hdmi0.group=1 video.hdmi0.mode=4 $ pibootctl diff default +-------------------+----------------+--------------------+ | Name | | default | |-------------------+----------------+--------------------| | video.hdmi0.group | 1 (CEA) | 0 (auto from EDID) | | video.hdmi0.mode | 4 (720p @60Hz) | 0 (auto from EDID) | +-------------------+----------------+--------------------+ $ sudo pibootctl save 720p $ pibootctl diff default 720p +-------------------+--------------------+----------------+ | Name | default | 720p | |-------------------+--------------------+----------------| | video.hdmi0.group | 0 (auto from EDID) | 1 (CEA) | | video.hdmi0.mode | 0 (auto from EDID) | 4 (720p @60Hz) | +-------------------+--------------------+----------------+ For developers wishing to build on top of pibootctl, options are provided to produce the output in JSON (:option:`--json`), YAML (:option:`--yaml`), and shell-friendly (:option:`--shell`): .. code-block:: console $ pibootctl diff --json default 720p {"video.hdmi0.mode": {"right": 4, "left": 0}, "video.hdmi0.group": {"right": 1, "left": 0}} pibootctl-0.5.2/docs/get.rst000066400000000000000000000056361372751746400160100ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . === get === .. program:: pibootctl-get Synopsis ======== .. code-block:: text pibootctl get [-h] [--json | --yaml | --shell] setting [setting ...] Description =========== Query the status of one or more boot configuration settings. If a single setting is requested then just that value is output. If multiple values are requested then both setting names and values are output. This applies whether output is in the default, JSON, YAML, or shell-friendly styles. Options ======= .. option:: setting The name(s) of the setting(s) to query; if a single setting is given its value alone is output, if multiple settings are queried the names and values of the settings are output. .. option:: -h, --help Show a brief help page for the command. .. option:: --json Use JSON as the output format. .. option:: --yaml Use YAML as the output format. .. option:: --shell Use a var=value output format suitable for the shell. Usage ===== The :command:`get` command is primarily of use to those wishing to build something on top of :command:`pibootctl`; for end users wishing to query the current boot configuration the :doc:`status` command is of more use. When given a single setting to query the value of that setting is output on its own, in whatever output style is selected: .. code-block:: console $ pibootctl get video.overscan.enabled on $ pibootctl get --json video.overscan.enabled true When given multiple settings, names and values of those settings are both output: .. code-block:: console $ pibootctl get serial.enabled serial.baud serial.uart +----------------+-------------------------+ | Name | Value | |----------------+-------------------------| | serial.baud | 115200 | | serial.enabled | on | | serial.uart | 0 (/dev/ttyAMA0; PL011) | +----------------+-------------------------+ $ pibootctl get --json serial.enabled serial.baud serial.uart {"serial.enabled": true, "serial.baud": 115200, "serial.uart": 0} Note that wildcards are not permitted with this command, unlike with the :doc:`status` command. pibootctl-0.5.2/docs/help.rst000066400000000000000000000066361372751746400161620ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . ==== help ==== .. program:: pibootctl-help Synopsis ======== .. code-block:: text pibootctl help [-h] [command | setting] Description =========== With no arguments, displays the list of :command:`pibootctl` commands. If a command name is given, displays the description and options for the named command. If a setting name is given, displays the description and default value for that setting. Options ======= .. option:: -h, --help Show a brief help page for the command. .. option:: command The name of the command to output help for. The full command name must be given; abbreviations are not accepted. .. option:: setting The name of the setting to output help for. If the setting is not recognized, and contains an underscore ('_') character, the utility will assume it is a config.txt configuration command and attempt to output help for the setting that corresponds to it. If multiple settings correspond, their names will be printed instead. Usage ===== The :command:`help` command is the default command, and thus will be invoked if :command:`pibootctl` is called with no other arguments. However it can also be used to retrieve help for specific commands: .. code-block:: console $ pibootctl help ls usage: pibootctl list [-h] [--json | --yaml | --shell] List all stored boot configurations. optional arguments: -h, --help show this help message and exit --json Use JSON as the format --yaml Use YAML as the format --shell Use a var=value or tab-delimited format suitable for the shell Alternatively, it can be used to describe settings: .. code-block:: console $ pibootctl help boot.debug.enabled Name: boot.debug.enabled Default: off Command(s): start_debug, start_file, fixup_file Enables loading the debugging firmware. This implies that start_db.elf (or start4db.elf) will be loaded as the GPU firmware rather than the default start.elf (or start4.elf). Note that the debugging firmware incorporates the camera firmware so this will implicitly switch camera.enabled on if it is not already. The debugging firmware performs considerably more logging than the default firmware but at a performance cost, ergo it should only be used when required. Finally, if you are more familiar with the "classic" boot configuration commands, it can be used to discover which :command:`pibootctl` settings correspond to those commands: .. code-block:: console $ pibootctl help start_file start_file is affected by the following settings: camera.enabled boot.debug.enabled boot.firmware.filename pibootctl-0.5.2/docs/images/000077500000000000000000000000001372751746400157325ustar00rootroot00000000000000pibootctl-0.5.2/docs/images/setting_hierarchy.dot000066400000000000000000000013531372751746400221570ustar00rootroot00000000000000digraph classes { graph [rankdir=RL, splines=ortho]; node [shape=rect, style=filled, fontname=Sans, fontsize=10]; edge []; /* Abstract classes */ node [color="#9ec6e0", fontcolor="#000000"] Setting; /* Concrete classes */ node [color="#2980b9", fontcolor="#ffffff"]; Overlay->Setting; OverlayParam->Overlay; OverlayParamInt->OverlayParam; OverlayParamBool->OverlayParam; Command->Setting; CommandInt->Command; CommandIntHex->CommandInt; CommandBool->Command; CommandBoolInv->CommandBool; CommandForceIgnore->CommandBool; CommandMaskMaster->CommandInt; CommandMaskDummy->CommandMaskMaster; CommandFilename->Command; CommandIncludedFile->CommandFilename; } pibootctl-0.5.2/docs/images/setting_hierarchy.pdf000066400000000000000000000224451372751746400221470ustar00rootroot00000000000000%PDF-1.4 %µí®û 3 0 obj << /Length 4 0 R /Filter /FlateDecode >> stream xœ¥WMs7 ½ï¯àQ:˜!$¯jÓÖ™ñô#ºerÐÄk'©VžÊNÚüû‚ÜoiåÕÌŽÇcÒx ÁGàûO*ýÕ›QÏ…QF1EÔ±T…Ñ ‘MPF{ÏÖƒ ‚–²×+Ö¿~•…ÿ•ßÉï×âÃG1u_Xu§¬BŒÊYuCœ¶Ùä}MZq³- žÈ mB Vð&h§¶UñæáÆÜˆEmŠ«÷åËË—Ããúãö]ñv+ÁÙ^¶rFKáxt6Ö±^2¦PüWa QT;ã¨$ã˜-(d<kõû÷ò¸ßýXo¿æ ²_Ú‚BËB¨ªž!«½â´4“÷=cH›UÊ ñ-Šm¯>'àÍR)$G^fHA`ÍÑ#³°§îªaåµ¥ÕºKk™¶ƒ(·àCØj'™Tϧ©é1ÙÃ=:kÀÌͽ„r©F¬’pgˆ!&í‚‹©¨,]CÌía}C«—3~Èy©2’eX¢G‡y´¯Gh4¸À:—.²Á4¾Ðz  kâ鿌)j’Ìli‘‹9ÖwvUmžžöL‘ƒ #Jôd$à<›æ§C„:ó0=z×ÔØ)~ ?Ñ*td?/; ½h‡‰užŸsóÓSUíròÜŸQ²¸ì£ÉåBI³èóq’`P즗(Cl#?|cCütÐeâ“ å}œŸ¨Ù;/É f¨¹=œ—VÚ,IxSKm›ùéi1ruI¬Æý ¶–žzYAq"ƱŠáñ±$¿Ü`æ—5ÙÕÓq°úTÞ>dqUž3%AD«ÓåTyh›#£Åfv‰²a:ÊzÞê” ñË(£,Ó€ó½:˜Ü9ÏëôÝîùï»4Ú=¿”Ç ®P3¦…lÎ*ö³ž]¢¨E´ =zkOÑ¿„"glM™9Šœ´eÌ1"^IÑÏßÒ°ª~œ1ä¤K‰-AVZb$û A=¢>°¼ÞÚtŠ_B4©˜ååPCê¾,Ï–‡Õ—}yØU絖¦ü)/Çb#½òU×Ì.IwFP褻÷èýÝ@º‡ø…ò$kX9¬äÇ%O†T%LvÒ£§ÿÓþÛ})„qL„M›Õ$¹”›©6ðÚÊç'¹‹ÝQ©;ð¾³Õoþº)þ,þG¢tí endstream endobj 4 0 obj 1051 endobj 2 0 obj << /ExtGState << /a0 << /CA 1 /ca 1 >> >> /Font << /f-0-0 5 0 R >> >> endobj 6 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 638 368 ] /Contents 3 0 R /Group << /Type /Group /S /Transparency /I true /CS /DeviceRGB >> /Resources 2 0 R >> endobj 7 0 obj << /Length 8 0 R /Filter /FlateDecode /Length1 8480 >> stream xœíY{|TŽŸ9¿™³ç±¯³ÙÍû±ÉæµHš <ÌŠ€<, ‘`Ñ!j " LÄ’A #BÄ€#RÜ`D¨‘‡‘ÚVð–KU,õ6bÚ‹Ú†drgÔÖÚÏýï~îçsÏÉ윙9gæ÷þ}gB(!D#•ˆwî}³Ë~·qG!ž „H3ç>ø€—,ˆË#$¢‘*JËî¾oñ…m²ÿî{——Fþáè1|n"Ľ}þ¼Ù%Ê=›>'$ç Cçc‡m§e-¶×`;yþ},{<Û¹Ûø>yàÞEsgšî%$ Yvßìeel­¼ÛÛ°í-»^ÙHË_ð1&HŸO$R*êX)oDj-$:`e׈|*¼Bb$³ílLjólÇÙŽ¬0#ÑHI4Ké^1ÝŸˆ:‹ýo½_öJª{?f»x'I#FÙ¬’]—ââU²hRBBüMO`J<Ϻ·DÖ¬–lIyÒX—¯é 1’ed‰r'¥;/´utw\2\yxáÒ—®^êp^ùòŠó„‘—5yÚ ‹Óþ>öWEI~JgNðgúä‡YÔ3˜ú’d;<ÆS›%&¥¦åÆÓœì¡¹CR3é`š;$9';œMXòÎ]»_yhÏÃý‡ø@\^øyeyÇý/¶V×—ô6øbÁoyã[ÆV>8w^BTÆùÃçÿ•ùËqã×<ò“ ‘ƒŽí?q)ÕI#߬œ¸I ) $U«”5Üóå-VúZd‹+h]ã‘B&K.ǸXgwÇÕŽ6d0Ä]Ç%缯^Áެ€??®,®!î—qq<ŸäÓ|)ß“ÃZ2•Lu ¶ˆ,¢‹¤EžE1ê¬ÅyM 16Ìc§>/1œ$'›XBü[XE÷Aë™Wžœ3÷—÷ˆ«â$õwD-Ai÷šú»tçÌ£'‡ 90` N5Fo´m=t`š$†Œý•ï adx Êμ@ ÚªTk:*”Åé²w;»Gµá_¶©¥ÌŽ«£Ú:²¼¼¬ƒ/yPÔHô$îð‘ÔãKJÍ5|¹9}ˆ–‹Õ“—¼þú¹]ÕÕ|‡øù†ž†š)õ;-o 7 E¢<#ËewÈ3vù({™´Jœ*ŒŒWpÉŽl\íRwGV@wªµ@-VËTNg…å>#Çã;Ä‹_kÝŸš¼!„• ~ÂÈÐ@¨ìT®¶Ak«F%…L±©ŠnòÒ‘} =sÔ¥«m† -íP±ç]drâ3†¤šZàjÚNIpÅŠÚ¦––1¯,=~Bjìù±´c玣=Õ¬øÀ¼’ÏM»ÓÙL\W'édRÀeS]Uaá-hIõÓZÕÇëÑq©QD±Þ,»\Þq~gwÚE¶É_Û¥“§8gZE^Öáâ•RÓGŠ;µᔾ±íÑM I2œ.¤/"7ví®Ý²{÷–ÚÝA!ºf7MºãÖŸÊ;¸âÝÝ¿Xq0/(>uá©“.üY|$>‹eà€×߸cî:‚etÄœ¹ûL},éý˜ŸG¿ÖIñÂäZ©µ>éZ©F9r ÊãŒDÙuwô™²óJMê7CüM ™dbèÒ.ˆ .P*z/à"ËÄqB¼%ªér~‹ŠOÄEN Ñ4†Nhwˆb§¸ƒ6Ò9xï&¦.G£.+X1‘ɵ@Œ3É 7+d"SƒyŒä .s(gÄâ<;¹Y+œÑ‚†MnvNnöÞavô^Ô²ÞìþÈÂ?³8•þÂ?+J¢'&H ¥r©Bª’*¥'¥FI1RAeôõhˆf©$•úÁϼJ.É¥#`ËRÆ“ñt"Ldãù9 L'Ói±¥””Ò°€ÝÍçËÅÊRò-‡r¶”?,¯&«i Ô°^%ב:ºUª‡mlß*ïåÏËÍÊ1å¢Ò«Ü0‹ ¥«4‡úF¿Iï¤w¾)~ÜÅŠ» ¡éZé·w˜„vg¬€[Ö-ÄСÚT[-š¬e¼Ë4³a :ûŽt„í 3-½/p~cæ0)aâÀíÏ·´Œ8²:lp,ríG{¢‘—Îå<´Þ7þšô*Ù*Q…ŒgNÓ2ÑEmN༘—ñN.÷9):¨ìþ{‡i[å;!­I!­´¨=Bµ“"ä»á­J8Ûâ ë"¬$"M?MÅ=.gç,ú‰ s•“ÄšÈ6º…ØÝ-\YgÒ­¸VºÙpéãâB ±>ÖvX|e|C<„¼®?ÄK! N¯Ë vƒ#^^ÑÞKzÛW¼ÜsòùM›öîÝ´éy8,Ýù÷޽%³éXªà=v¶ð´_¾ÜŽ¥Ï–ùH´A ¸•ZéEFÕd ÄòáªiØÖ>ƒËáS|È:X`£¡lšhƒ¿hÑÒïß~»' íºg»TÒ•!ìÉ3u1í{9æ ÞÏ Ù"3˜Å¬8£C’¨nà›š¡jÔ¬tÍ¢XTCQ,c4 £LA<%õ?!ü±^OŽÂ;LÃ#”Däo%9”DÎöe‘LDíß“@¾‹¸¶iŒiÑÌ£¥j£Ù´ÛØí–Z©ö }˜=hy@[ÏViO±l«e“¶QÛC_`/±Ý–ç´-VƹªéÑàá5Z÷C*OQè^ÛšÃøËP5OϲM„ñ|œ:IØŠLd&Á혊07LW‹ôÛ"Û2Za{šn±ì§–fÛ»¶‹¶^[&Æ{Uò©ÿ0Ö³qÝw^GÎÓWÄý穟úYqÏÅžã4(&H“¤p±˜nèU̇úU‰?à’k™TKe/*œ¢Š ÓLížm m¢L=èÐMåšû'³øNÃÌž2© §u»OLØ×3Ìœm.cˆ"ùRE×0{•mÖb°–ˆ éd.™à‡Nv麓9ÅÕ+Î/®˜i$ÆS³1¦!†Óom˜¯ï$“ú .Oy¦à•'^)xfÊwÏêïÓAT¾mËmÊÈøøÌ™32ö%'c ·Sá»—åäÕJÞ -ÉÄèŠY¤j*54ME»’@ò’¢sU1‘º&Dz4”†-´è73ÅükÃÁm™Å˜‡âP%Í#¹-aZª”jñZR5¯6Ä’«-VHå–åZ¥´Ê²JÛ(…3ªCiˆ0†ÐQ0])Rç) Õ•åê£t=ÔÒ§ÁmâìD•¢ø ŠÜw9_I+è ·DE»¨hãçºø[WOè&Œt}â}êä$ê$\î?™Ö2í›#ÔU sWyj"M]¥¿9¹5F±[wÒ¸t£Ÿý‡3TÞ¦ò\ÿxrý„¤™²¸/V‹Õc­ƒÕÚ@} u¤:R©´ê^âET•®¥ëÂ2Ý™žáéñé ~¯?19­J«Ò«¬U6—É$Éš¬ƒl`8! ¢!bYœš–éÏ÷ßå¯ðWú7úüþHÑâ>l‘}ß=lŠ1Ÿ²wfMÍœ-ùm»¿úÍÌ7ï-=1{Õºyûû·ý᥇XþôôÂÂÀÄDû€§j¶öùŽææM\âH®]µ£)ž\ßÛ°í¿5Ü-ø7yCÆ^ßN÷c.´’­ªÌ)“s7¢÷¹—ч.]—ûVüa¦99­¦ïb’¦„Ki’Ÿg(Ó¥Réne‰ô_%Õð'”ÍRߪ<'¹T®Ê’š%ÒX:Ï3,ë|(¶ÖÀjÜÁ­—7Xêa«e<Ï[Þ²¼où :á+ÖÉ¢g-67p†@L³:Ò"¥ü¹ç€tOgÏÉÙݽ€~Üsµ§Iòõ|Їys?6𘠱¢"95%f› ÑÊUF^?¹Š¹¡8,BrúRfJßöÛOéæ«4—&ˆE»ƒyñ ­óE˜Í3¯=D#QUiıUTŠGD](_À|9#tvâ&#±ßœž¬Óh«;hÅ=™[Ÿ"¡=&y}ûJÜ2\?BYä9f¡„a–êK’_çÎTzÀ­1OS­„IS°ŽÇì ÄN*H/FgÓeôºI:!]ð¦z³¼#¼M‰I½½æ9'i ·Òb_Ù?†ãy_ÿEq ´žn§;ðnè¿Oà}Šžú·_þÏ/'q‘¬Ã±¸‰£¿7‹…Øú©øþKÇZ‘°–±0,ÑèvÂÑRÌKżˆr|ö ìþÿúÎEsI´ã}œì#Ûél•b÷bìi’Õd)ö¼IÛi4ûöNò¾YMÚa#tÉÁ^BÎs‰\¥…äΑ‡8‘aSØ!v+ ²Ëì Æ–°3¬˜-¡9°‹Oç{°äÁ[’ ý;éE²„O!ZÙXf'á ì#Ÿà*¦^ÛÉÒHÊ‘7]D*¤réVì9ÉÏz¼áø´Ò÷º#ô1rŽl&M ;è9ä«|IƒB©7:9R)Òç:ƒßד%¸Ó @1Ù€s×›ßÈ¥Ñgè.7g—bÅtù”[æàÜo™ᚇ¤[‘£RÒŠå!Ù‰<¤«¡)5GãÈË$–‰ßã –•È5!‹ —,ħrr€$ƒ ŽlÀ™BüÊÃø—øåvö!ò¼®—¾$g`,ñ“RveG0¾½j‘9CèAzÍRÊÄ’æÀÔÞSE‰ƒþSÓë´x›IA³m¹7ØÛ[0ƒÅð¢fÛ )J3Kñ}ø}ƒ8¹`†·¹gÜØþYÇži3ðÑla7ö3mæ)ø7±¸Ù;w¾w­s­oÄZ缃ú¢Tô”ÞØ=ü.Ǩ/H‚²á_ýô«ÈëõWïwßb/R/`Óì‹øk¹OÄb_½ß5Õ^ô¸‡ZÊݤšuÅà#ä¼€D¿Ð{Ú£FÚø¨@¯FÚ`„€<ǹùð.6Ôɇ¹ah®Î‡:!W‡!ñcƒìè<[ÀtÈÊÔy– 2u> stream xœ]R»nÃ0 ÜõÓ!𣊕‚"]<ôºý[¢Sµ,ÈÎ࿯()ÐÁ扺;R¢²sóܸq…ì=̦ņÑÙ€Ë| ¡ÇËèDQ‚Íz[¥¿™:/²(n·eÅ©qÃ,´†ì#n.kØ`÷dçdoÁbÝv_ç–SíÕûœÐ­‹º‹C´{éük7!dI¼olÜ×meŒÏÍ#”i]pKf¶¸øÎ`èÜ…Îóô0Ôý·WYÒæ» B#5Ïcº:$ƒÐJ&CÌŸ8"ŒŒ‘°b¬"–CÂ1DmÅÚŠpɸ$¾a¾!\0.ˆÃþŠü9÷“Ç2Ç’?s$q${ÊäÉu+ª+ù,2…û¯¨yäü‘j1V„%÷)©Ïеi%וTWq^%Ïž9=qØ_’¿â{‹.üv³tõôFî35×â8ÓCJs¤ ŽïoÍÏžTéû6^°u endstream endobj 10 0 obj 356 endobj 11 0 obj << /Type /FontDescriptor /FontName /OLJEOO+DejaVuSans /FontFamily (DejaVu Sans) /Flags 32 /FontBBox [ -1020 -462 1793 1232 ] /ItalicAngle 0 /Ascent 928 /Descent -235 /CapHeight 1232 /StemV 80 /StemH 80 /FontFile2 7 0 R >> endobj 5 0 obj << /Type /Font /Subtype /TrueType /BaseFont /OLJEOO+DejaVuSans /FirstChar 32 /LastChar 121 /FontDescriptor 11 0 R /Encoding /WinAnsiEncoding /Widths [ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 686 698 770 0 575 0 751 294 0 0 0 862 0 787 603 0 0 634 0 0 0 0 0 0 0 0 0 0 0 0 0 612 0 549 634 615 0 634 0 277 0 579 277 974 633 611 0 0 411 520 392 633 591 0 591 591 ] /ToUnicode 9 0 R >> endobj 1 0 obj << /Type /Pages /Kids [ 6 0 R ] /Count 1 >> endobj 12 0 obj << /Creator (cairo 1.14.6 (http://cairographics.org)) /Producer (cairo 1.14.6 (http://cairographics.org)) >> endobj 13 0 obj << /Type /Catalog /Pages 1 0 R >> endobj xref 0 14 0000000000 65535 f 0000008897 00000 n 0000001166 00000 n 0000000015 00000 n 0000001143 00000 n 0000008448 00000 n 0000001275 00000 n 0000001489 00000 n 0000007697 00000 n 0000007720 00000 n 0000008154 00000 n 0000008177 00000 n 0000008962 00000 n 0000009090 00000 n trailer << /Size 14 /Root 13 0 R /Info 12 0 R >> startxref 9143 %%EOF pibootctl-0.5.2/docs/images/setting_hierarchy.png000066400000000000000000001164411372751746400221620ustar00rootroot00000000000000‰PNG  IHDRÌ[S~3sBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î< IDATxœìÝw˜TåÝÿñ÷ô™í½/»ôÞAŠ¢`A±aEÔ(Æhc,Q#Éc~ Æ4}ŒÑØ Š=VŠ(M¤.½-ì²ì.Û{™úûca`i"ì0Ëîçu]^2çÜçœïÌÎìÎçÜ÷}ŽÁï÷û "c¨ ‘ÎOÁCDDDDD‚NÁCDDDDD‚ÎêDDDNµ'ì ¼¾%ÔeHüôìt‹ u"rér>ÙPBqms¨Ë ¹fd†‚‡H¤¡V"""""t """""t """""t """""t """""t """""t """""t """""t """""t """""t """""t """""t """""t """""t """""tæP ""ÒQ¥DÙ¹rXÝâ¨szX¸­ŒåyUA=æG?Ï]³sØ]ÕtRûéÁ9½ðúýì­u²p[-_{”yÒ.èŸLm³›ùßýzšR£íì©n>•‰H°¨ÇCDDäÆdÇññgÒ'9’œÂZš\žš6Œÿwqÿ 7=ÆÅtòž¤FqûY݉r˜IŒ°ñ“ ÝùàŽñDÚ:Æ9Ç©CS™Ð+á¸Ú¦FÛùâî A®HD‚­cüöé@ì]3„×Wðä‚åŸl(ὟŽcy^ó¶”2 5мŠFšÝÞ@›Ôh; ×¶ž´›’Ùh$§°†Úf76³‘ž‰l+©çŒîqØÌF¾Ú^~X-=#è™N³Ë˺ÂZꜭۧDÙ1gÝ=ÂÙ¼·€šf7Ÿ¿#°nÁ½gsÑÀæ¬)$;>ŒÞI‘¸½>ÖÖPÝäì§GB8 ."¬ôO‰dqn%õN7ýR"ÉŠ§®ÙMNa M®Ï{@j;ËEr”•ùUT6ºˆ ·2:+–âZ'Šjøz›ú&G²µ¤ž‘Y±D;,¬.¨¦ªÑx ¦FPPÝD½Ós\?Ké8ßTÂUÃÓ˜·¥”'÷áËm强|w Í³×ç•ovóÁºfFgÅò·«‡SXƒß_6€ŸÏ^ËÆâ:Ò¢̾m ËvVPZï<,xL™Á5#ÒÙQÖHL˜…?]1ˆÛ__ÍÖ’z†fDs÷¹½¸ô—â?¨ý¹}¹å•U‡='§ÛKec Ñ S¦ðã ÝÙRRO„Í̦ä®Ù9¬ÝSÀ£W ¦²ÁER¤‚ª&¶–60c\gdDz»²‰Ôh;Yñaüàß+)©sðŸÁ’•XŒ ¿½¤?ÿï£MÜqvvU42¡WÏ~½“×¾-8¬¶˜0+s~<–¹›K±˜ŒXÍFf]:€kŸ_Nqm3W Oà¶³ºðâÒ|69ĈHÇ¥à!""rˆž á×4Óè:ü¬úöÒ®™À9ÅÜ<6+<ú¥D’ÆÜ-­_ ÿ|å ~ÿɾÜVÀ5#Ò™ya_nzi%Ðz¦ÿ‹Í%¼ŸS|Ä:ÞÏ)æíÕ…Ç·ÙŸLèÁ/æ¬cÁÖ2þgJ?FgÇæIL™Á³_ï ´7 ÊnÁl40±o"=ÂYµ»šM{ëølSI Ýu£2ùÙ9=¹ýõÕ¶5¸öùåxý­±æ¹E»xzáù!¿¹¸?7Íâ¯s·–mÞ[Ç3_µÿÅ›Fò« ûrù³Ëhv{™Ô7‘‡¦ô;bðØoÉÎ ÞYSÀ3×çÊai<óõNþ6wç÷KâsÖu[éø¬Íqçn. „‹ÉÀõ£³èŸ‰Ýb"=Æ µßÂmzlv”5PRç CË-k óùüÚ.Øz ö­%õ¤ÅØùÚˆÈéEÁCD:½—_~™»ï¾;ÔeHÒã¢b;ý¨ë÷T5“cÇb2âö¶½ Tv|8U­ó*š\^¾Ø\ÊÃÒx|Þv.œÊ}ï¬ ÊnÆíó·™ÌíóÁ3_íĸ/w¸½>œÍ9Ô¯.ìK÷„p^ùf7 - ïÃMcºÖÏYSħ??“Ø0 ÓFfðAN1®ƒê-ªqrá?¶ß»&õbtV,/,Í£¬®…¾)‘Ü~Ÿ6mê™CñØÕCpy|¼½ºÚf7 HfhFt›6?¯ÏO³û@-Ÿ£Á€£kvµÝþà€&"§?éúöíËçŸê2¤ƒ¸eöVÊZ޾þëåü–þL™Á+ Jˆ°qéàTýâÀð¢rŠxâÚ¡¬)¨¡Éíeå¾aO»*±˜ ÌÝRJÁ ^÷¬^ üö£M|»oŸgr¨Ò:'ËvV2c\6ôOâŠç¾9¾ýöŒç¹E»=Ã2cŽÙÞœÙ3žËž]x.û‡› .ok‰ÉhÀ{”ž(éø?o|»§Í~—í¬Äéö1eP S¥eçÛ¼*ò*1 „ÛÌ,ÚQÑf_‰6¾ÙUI£«µ÷ddV—MÃj2ðÌ×;qy||{ÐM ‹jœÜ2>‹§îdgyc`¹ÝbÄãõñ†‡«wW3(=š+‡§i7óôÂx|~–î»ÂV\˜•Åu”7ør¿"¿Šsú$rÉàT[<ü{Y> -r ku/ÝY¸Än„ÝLY“e­áÄd4e·ðõŽ ü@´ÃB^e»*1 D;,|µ½œýaV3UMn¶–Ôã÷Ãò]U J‹¦GB8;+©hh;¿ä`W O'5ZóCD:ƒß”^""ÄÓO?ÍóÏ?φ B]Štç?¹¸Íý/Ngç÷Kâ¦ôcòS‹:!¾«yýÖ3ñÃÇDäÔÓP+‘Óø÷ŒQ Lâ÷ŸlQè‘OÁCDDä4ä›»ÊFWà&~""™‚‡ˆˆÈijÓÞºP— "rÜŽ~÷$‘v¢à!"""""A§à!"""""A§à!"""""A§à!"""""A§à!"""""A§à!"""""A§à!"""""A§à!"""""A§à!"""""A§à!"""""A§à!"""""A§à!"""""A§à!"""""A§à!"""""Agu"""§Ú%ƒS(¯o u$ áÖP— "G à!""]Î/ÎëêDDº µ‘ Sð‘ Sð‘ Sð‘ Sð‘ Sð‘ Sð‘ Ó}¯Í\pŠªét®¿þz<ÏQ×[­V®¿þz,Ë)¬JDD¤kSð‘NgĈôèÑã¨ëÝn77Þxã)¬HDDD‰ˆˆtm "Òii¸U÷îÝ:thˆ*éº}:n·;ðØn·së­·†°"‘®KÁCD:­ÔÔTƘÏát:¹îºëB\•ˆˆHפà!"ÚŒ376l½{÷qE"""]“‚‡ˆtjW_}5>ŸƒÁÀŒ3B]ŽˆˆH—eðûýþP!"‹Óí¥²ñðKÑž®f\w çÏeÕæ\’’SB]N»°[Lć[C]F‡Wßâ¡®ÙýÝ ¥Ó3›Œ$GùÞ>"rj˜C]€ˆt<_m/ç¾wÖ‡ºŒvS5GV 7ÎÞìu9íâŒì8^ž1*ÔetxÿY¹‡'tŽŸ¹œœ´hóïê2Dº49"‹Ñ€Û×9:D#ûƒNvÃ@¯:«›ÕdÀåÕëÕÕy:Éï3‘ÓYçúK,"íÇ`uíÆ`¶5@g:»*C'z/‹ˆœÎ;Ÿ?(Ç’ÍôÑ™,ÜV~Òûzý‡g0¢[ 5Ín6Õò—/¶±«¢ñ¤÷}²âí,~`bàq½Óæ½uüùó­ì8ÉÏ0@¯ÄpîšÔ‹Ï6•œô¾D$¸Ôã!"'lêÐ4~sq?þøéVÆþåKÆüe!Ï|µ“¿^=˜szŸ>gr/zz Ãþ0Ÿ{Þ^Ç…’¹ý¬î¡.©]¸Ýn>ù䮿þzY½zu¨Kê’£ì¼4c[KêýçLxì+¦¿ð-çöMdÖ¥B]Þq{bÁ<<—+Ÿû†'§ uIm\ôô<<—KžYJY½“G¯ê’DäS‡ˆœ£ÁÀ=çöâ¥eùÞ ¿ßÏ는ͽçõâëåüp\6.¯7V¶=§w"gõŠçŸmàü~ILIb„õEµ<1UM.þzÕ`>ßTÊÔ¡©ôMŽä¦—V¶©#Òfæç÷fpz4V“‘ Eµ<õe.å -tçÉ}¸ç­œÀYk»ÙÈS× ã÷ŸliÓ«áñùÙPT˧K™‹Ílä ú04#‡ÅÄæ½õ<õe.ŵÍ\=<(‡…›™ ú'ñú·|½£‚»'õd@j~?¬.¨æ_æRßâàîI½Ø]ÕĨ¬XFt‹a{i¼™3{Æ3c\6~¿Ÿg¾ÞÉ¢'üsñù|,Y²„7ß|“Ù³gÓÐЀÁ`Àãñœð>;»ÛÎ̦¬¾…?~¶Ÿ¿õ}²£¬ßþw3/Ý<Š—æn3sëø,îg=ûû?"ífž¸v(¾»ª&™±aÜ}n/ú$EPZßÂs‹v²¦ hí¥³›Ä†[9¯o"ÿ^¶··m¯Ú £3¹p` ñáVöÖ:y~ñ.Vî®Z?o®ØCNaM ýÏ'ö¤ ª™Ö·ÙOi“/Ëç[Ï Úaá¼~I\68•ÄHeõ-ü{Y>Kr[ßcYqaÜAÞ_[Ämgu§ªÑÅ]oåpç9=×3ž‡…Ý•M<ýU.[KêÖ¡ŽgõJ  ª‰i#3hv{ùûüT5ºøåä>dÄ8˜¿µŒ§î ¼ž«hháÓ%œMr`™¸qL7¦ LÁj6²4·’g¿Þ‰kßkfá¾óû08=šªF¯[À—ÛÊNð'."¡¢9!Ùña¤DÙ™·åð?þó6—Ò79’Ø0 ¹å üèÌlLC`ýÆtco­€)S˜ya_^X’ÇÏþ³–ºf7ÏÞ0œý­Gt‹á—“ûð~N1?Ÿø¿ŸÅldcq¿|w=?Ÿ½·×Ç_¯j=“š_ÙHv|çöM ´¿p` I‘6Šjšú¼jšÜ˜FrËùÕûùé›k©hlá©ë†êÊŒ ãÎszâòø¸÷íu,É­Äa1²tg%w¿•Ãï®')ÒÆ¯§ô ì»oJ$÷Ÿß›Å¹Ü5;‡˜0 ÿ¼qãzÄó«÷7ðΚ"þvõ"lßÿœÐ¦M›˜5kݺucâĉ¼ôÒKÔÖÖâõz:¾Ã˜îqÌßRzØ—äùUÔ:ÝœÑ=Ží¥õŒëϰ̘Àú)Sˆ´™©jrfeömcÈÙSÃo®å­U{xöúát‹ Z¿àß5©õN7÷¼½ŽovUVG‹ÇÇ£_lãG¯­æÃuÅ<=}ÉQvŠkÜ8¦[ m´Ã­gvgCQíŸSV\.¯†^ŸŸÇçïàG¯­æ?+÷ðØÕƒÉŽo­+ÂffbŸD®™ÁŸ>ÛÊS_æ­Ãµ~ÿñfn{m5ßäUòü#³šHŠ´qíˆ 2bÜ7g=‹vTð÷k†0ó¾<¿8‡>ÜÄ•ÃÒ9¿_R›šz'E005Љ}ùñ„ü÷ ÀtËølnÓÇçmç>ØÈèìX~wi õ$Ç¿~0³ÑÀÝoåðæŠþrå Æõˆ?ŽŸ®ˆt$ "rBbì@ëÙÕC•Ö·.‹ ·±lg%Fƒ1Ýã€Öa-gdÇòßõ{øñ„î<±`Ëóª(ªiæñù;ȈqÐû Éã¯-ßÍWÛËÉ-oÀéö¶9VU£‹÷ÖQïôf5óñ†ÎÈŽÃn1áÞZUȵ#3í¯‘ÁœÕ…möqëøl~yAþ}ó(ÎÈŽãßËòityxkÕjšÜDØÌ|¾©5L%D˜@¿­´ž.ÞÅ®ŠFŠk›É¯lâ³%´x|ØÌF>ÙPrسO7–0ws)»*™½r½’"xäÓ-ìªhdΚBZÜ^z%ßÄùÝ»wóè£Ò£G ÄŸÿügŠŠŠðûý¸\®ãÚ‡@\˜•²ú–#®+«o!>ÜŠËëãÓ%LšX7uhïç´~y¾nT+ó«xcEŵÍ,ØZÆgK¸lHj ýÆâZ^\šÏ®ŠFJŽð¹ywmù•DØÌäU4²³¼‘1Ù­Ÿ›·Wr^ßDbÃ,c¯+¬!¯òÀ޳z%pßù½ùË•ƒøí%ýy~q^ŸŸ×“[Þ@¸ÕDqM3Šê8³gB`;³ÑÀÃofÓÞ:rË[ç\¼±¢€½uN"ífÖÔÐìö1(-:°Mm³›Ççï ¯²‘–änåíÕ…¬ÝSÃú¢Zæo-kÒ µÇoÖe¸÷¼Þ¤EÛYpÐÜ–œÑ'ä²fO ;Êøý'[˜:4›™a™Ñt‹ ã÷Ÿl¡ ª‰ù[Ëx{u!7ž‘ù?Yéh4ÔJDNHþž‡¸p+5Íî6ëâÂ[CI½Ó×ïç¿ë÷rŰ4–íªäò!©|³«Šò† @v|8¿œÜ—ûÏïC¸­õŒj¸ÍLzŒ#0y<¯²é¨uĆYxzúpbŠjširµ“Ä+{ª›ù §˜»'õ"3ÖÍl¢j$w¼¹æ°ç²?À<ðîzª]DØÌ<5m(é1 ªšhtyñûý$DX)ohý’šH]™±aüïôaøü~Jêœøý­õ™Œ¼û†zÜÓÒÐ⡼¾¥Í¯:§‡HûÑ5{ê*¨Û´ˆO^\Àë{r±Ûí8­_b¿+l<ú裼òÊ+ÇlÓÙ¸zAìØc¶©oñÞ³‡Š·R»ïýý~N1/Þ4’¿|¾•”h;R£¸ã͵tOg\x¾¸{‘v3FXÍ&¾8hÂó¡ï—C=pA.’Jny#µÍnR£í$F¶ÖU\ÛÌò¼*®šÎKßä3mdÏ~½³Íö-n/uÍ «›yå›ÝlÙ74êgçôäúÑ™ä–5PÛì¦[\‰è†O›àe2øÃÔŒíOnY -­ïÉ„ˆ¯Qqms ‡ÈãóÓìò¶yo×;ÝmB:À]oåPPÕúŒÈŒáå[FsñÓK(©s’eks±ˆÜ}ÿNqFau3ÍtØZRÏÙ½‘Ó‹‚‡ˆœå Ô9ÝŒïØ•sÎì™@aMsàËÌû9E¼óã±DØÌLšÆÿ~Õú…ÉÔ9Ýüêý |›WuÔcyqU¡›ÇfQZçäÿ^´A¹p@2ìUçtóù¦®‘Ãbâ³%‡ ×z{uaà Ñ~ÓFfàõù™òôü€ÕddõCça8hȘÇ×vŒþ{°$·‚¿ÍÛ@¿”H&öIÄpP›CŸIp®—$ßÇš‚Æ÷ˆç¹E»Ú,ï›I\˜•œ=­ó*6×RZïäÜ~IôNŠ`á¶2ꜭ¡¤¶ÙÍÇöòȧ[ŽzœCçt¬J$WOgòS‹ïÏ—fŒÂpлgöª=̼°/Šk‰ ³6Ìqåîj^Xš×fYf¬ƒÛÎêÎùO, Ì›zúºamÚúù:»OC2¢™üÔâ@Í_Ü}VÛ÷ñÞ¸GZv4köÔàõù˜Eá¾QÁ@M³›:§›(GÛ¯+ÑK ŠÈéCÁCDNˆÇççÅ¥ùütBÖÔ°yo#»ÅrÓ˜n<úŶ@Û]l/kàÁÉ}Iˆ°²ð I¡ ·•sóØ,Vï®LÛw–ùXc¿›9Ðû­C^5{ßx{‹ÉÈßXsØú#‰´·îw׌Ho3Oåhµì>è¬öôQí?Ä•@ܸ«9ýGüaR³gÏæŸÿü'yyyX­ÖcözÌœ9“I“&µ{MÙóKòØ|HÏÀ¡^Z–Ï;?ËÍc³xuùn µ§jÖ¥Xº³‚MûÞÛäså°tz$„óð'›Ën+ç±k†ðâÒüÀ,&#6ÕMßý9ÂfÆíõz¿ú$E0²[,KsÌY’[Éo.62ëÒ¼¿¶ø˜Af¿p›×è-ÈŒu0¾g<;Ë~™ÝH›…·ϾýïOflØwëû¸p@2“!PÇò¼*®‘ÁÚ}!oÚÈ vU4RVçÄíñã°2¾G<ËvUb3™:4­Íï9=(xˆÈ ÛµŸ×8š˜ ºÅ…ñÜ¢]¼»¶¨MÛrŠùí%ýùÏÊ=m†ý}þþzÕ`>»ë,¶”Ôn¥[\“ŸZL³Ï{è!óîÚ"^ž1Šô6³‘¢šf¼‡œzÝX\GI³ÑxÔɸ‡ú §˜7~t/Í´Þ{ ÞyìIÚ³Wîáï×eHF4ñá¶v¹GÁ±dee1sæLfΜɦM›˜3g/¼ðÅÅÅX,Íó8N¹å Ü5;‡G.ÈÆt£¬®…¾)¬È¯æ×ïolÓö¿ë÷ò‹ózSÕäbéΡ`Ù®J^Þ`6Õb0è—Á#ŸneÞqÜÓ&§°–¢'ïüd,{ª›I‹¶³î +XAë½fÞ^UÈ/ÎëÍÏÖ¬=®ç¶½´õEµ¼÷Óqì,o [\X çhn/ãŽszðÖíc©njvØïå—gŒÂíõí0ãñùyäÓ­9%ýbÏÞ0œ·oK“ËCv|8÷¾½?PÕäbÖÇ›yìš!l(ª%;>Œ‚ªf^ÙEäôaðû¿O稈tŸo*aæûëŒ*´{è—‰ÏïgKI}›ˆý¬&#ÉQ6ªÝ4º_Ÿã =ÖAƒÓöÒú@ïGj´êF΃ÂJ|¸—Ç’í°Ð79’š&ÛËÈŒu°·ÖÙæÆooÝ6†÷rŠykÕžÀ2“Á@ZŒý°¶ûEÚÌôK‰¤¾Åö’zÒb”×·àòúˆvX0 á+û%DØè™NI­“ÂêfÒbìV7ãß·®Åí Ôm7‰ ³¶™hœe§¦Ù}Ø$úCÌŠåµ[F¶üX—ÓýòË/»dÇÿ}½ó¸ni6Eø¾ž«ý=‡J¶ãöú©h8|Bz”ÝB¯¤pv­wD7hÓûf5a3†$šŒ§Ec4ÂÆ¢:"ì­½ ޙ͸ñÜöZÛ{²$EÚhv{ŽƒÒ¢°šl,ªÅa5á󷳚Œ$á*o6³‘AiÑx|>6Õa¥¡ÅC“ËK¸ÕL¸ÍÔf^HzŒƒ²ú–À?û?gû5º¼T5ŠÍF}’#±˜ l+=üBQv }’#¨jt‘WÑè<ÒçèH’"í|ußÙÇl#"Á¥à!"‡ù¾Á£#ËŽãÂ)ÜxF&“ÿ±ä;¿ÐŸ.Ž<æv»™;w.¯¿þ:}ôü±‚Çi*.ÌÊàôhþ|å ~ùîú6½-r|ÞL‹Û‡á;îÂÜøü>â­ß+xTTTP__O÷î݃X™ˆˆˆHç¢àÑùý~Ü>õxìw„ûƦ¾¾ž?üW_}•… òä“Orçw¿8‘NBÁCä(ZZZøüóÏyíµ×øïÿ‹ßïÇëõb2™B]šˆˆˆÈiGÁCä >ŸeË–ñöÛoóꫯÒÐЀÁ`hsã5›Í EDDDNO "À¦M›xõÕWyñÅ©ªªÂl6ãv»C]–ˆˆˆH§¡à!]–»¶ŒšÕŸ±ký|=P†ÕjÅårµ®;Fèðûý<õÔSÌ™3çT•zʹÍa0ñ¾P—!"""ˆ‚‡tY–Ȳ‡`j®¦qËbš››1›Ím†UMïÞ½=zô)¨24Šý¬u"""Ò©(xH×e4Þc8YCÆ2ïçï3wî\fÏžÍ{g×ëÅëõâ;ÂÕ¿ _|q§¾ªÕç›JXñþÆP—!"""ˆî\.Bë„ñË.»Œ7Þxƒªª*Þxã .¸àL&V«£Q‘“¡oS"‡p8\{íµ|þùç”””ðä“O2zôh v»=0DDDDDŽŸ‚‡È1$$$pÇw°|ùròóóyøá‡éß¿¨Ë:5üÇqgE‘Ó€_ïe‘AsL•Éš‚jZ<>~}Q_–äVòЇiñ~gïö0<3OÁCDDD¤³Pðcš>:“kGdpÓK+ØRR@J”—gŒâ¾óûðçÏ·÷¾ @l˜•ª¦#_ŠÖh0a¥¢ñÈëíf#á63•GY/""""—æxÈ1Ý2.›W¿Ý%uNž^˜Ë´‘„YML•É3×o³Ýý“yë¶1@k ¸ãì,ùå$fß>†…÷ä>‰¶ÿ¼q÷ß›Ïî:“w2Ža1möe6xçÇc™{ÏÞº} _ß`ûH»™¯ï?‡¬¸°@{›Ùȼ{&0(-ºÝ_91êñ£Š ³ë`õîêÃÖ­*¨Áf6Ò79’ù[ËøõE}I¶³·Ö ÀUÃÓør[9S‡¦qáÀ.{v)U.†¤GóÏGpé3K©ltn53¹27¿´’ÒúLHË>ÜĶÒÖð3¦{O\;”sŸXD½Óò•\;2ƒÇæmZCO³ÛËÆâÚ`¿D""""rœÔã!Gå°´æÒ:§ç°uµÍn¬&*ZXº³’ˇ¤ac|x>Z_ À•ÃÒøtã^­&2cT7¹ØSÝÄȬØÀþÞYSDi} ^Û» {ý~öT7qAÿdnÓÁiÑXMF²ã[{9ÞZUÈÃÒ°ì»9Ôµ#3x{ua{¾""""r’Ôã!GUÕäÂëó“egóÞº6ëÒ¢í”í äsïy½y~ñ..œÂªÝÕÞä(; HaLv\`ûz§¯ï@À(oh9j)Qvfß6†oóªÈ-oÀïo #Qv 9…5”Õµp~¿$¶•Ö38-Š»fç´Ï‹ """"íBÁCŽÊéö²ª šË‡¤òå¶²6ë.’ÊÞZ'»Êøj{9³.À°Ì¦MãÅ¥ù¶{k›Y°µŒ×¾-8ê±éähcÊÀÖÖ0óý @ë$ó»&õlÓföª=L™Á–’z>ß\JÓý=Ÿ­ˆˆˆˆ“†ZÉ1=>og÷Iä®I½ˆqX³š˜62ƒã²ùëÜmaQ.¯O6ìå—“ûã`þÖAeöªB~tfw¤F`2ß#ž¸pëqÕÐäöÒ-.Œ0« “ÑÀýôÁh4´ióñ†½ HâšéÌÑ0+‘G=rL‹kùá++¹ïü>üxBwv”6pÿ;ëX¸oòø~ïçsvïDæ¬.Äéö–¾©„0«‰Ç¯B\¸°±¸ŽßkíÁ(«wÒèj;¤¦ÙŠõaNçôN`Ñýiv{ycEk jÚ£Éåå³M% ËŒaížš`¼""""r ~ÿ±¹Hg4úÏ hty¿»á!ŒFx|'þ–1 '¼ý±¶5oÝ>–÷Ö1{Õžïµß„‹î?ç„j‘ã£9n>¿Ÿ“ÈÀÉ…–£m;¡W— I%.ÜÂ9E'¼ é6×ñÄ‚8=¾P—"""""G à!§½Å¹,έu""""r ºª•ˆˆˆˆˆ‚‡ˆˆˆˆˆ‚‡ˆˆˆˆˆæxˆˆH—ñÒ7ùì(murŠüú¢~DÚõUG¤£Ð§QDDºŒ¯·—³"¿:ÔeÈ)rÏy½ÝÂ{k‹Ë_ÿ¶€©QÇ6³‘a1DØÍl(ª¥¬¾%°®Wb%uN2btg}Q-E5ÍDØÌŒëOM“‹U»«ñïkŸë ÙíÃj224#š¼ÊF¶–Ôc1Û=£ÁÀò¼JZ<¾À1²ãÃè‰Ûëc]a ÕM.•Þ#!œŠ Vú&G²½¬å mžgflR#ɯl:æëf5‘ÆÎòÆöˆÇb2°2¿š†f£¬ø0,&#÷½6‡ç»Ä‡[œËãcížšÝ^ú§DR\뤶¹õyÙÍFz&F°£¬—×w¬]ŠH¤à!""rˆK§PVßÂ9Åm–ûM{ëHŠ´ñÂM#©wzØ[ëäSòÈ'[ødc ÿ¸nù•DÚÍT7¹ùÃÔÌ|o?žÐƒüÊFFeÅ2ws)ùb÷ž×›h‡…(»…ÝUMœÛ7‘?¾•K§RÕè¢gb5MYÜòÊ*¦ LáǺ³¥¤ž›™?LÈݳsX³§€G¯LY} qaV kšyäòÌúx3oØ ÀeCRyhJ?¾Ú^N·¸0Z¼G}=ú%Gòôôál)©£Þé!%Êί.´påsß`2˜20…(»…ÛÎêÀ_çn;î×ú¢)<4¥ßìª$Âfæw—öçÖWWSTÓÌ…RÛ#Žü{ŸŸß^:€h»…ŸÏ^{Üû‘ŽCÁCDDä=ÃÉ-oÀç÷µÍÏ'öbwew¿•ƒ8¯_š:ˆE;*¨oñPÓìægÿiý’üûËò§+qé3K)«o¡or$oß>†ÇæmÇãk=N”Ý /~‹Ççç†Ñ™<|é@n~y%« ªqXL,zàú¥D²µ¤žù[ËølSI žëFeò³‰=¹íµÕe^ŸŸ^ü?0md7Íâã {qXLüú¢~<ðî–äV`4xñ¦‘Ç|MbÃ,ükqßæWaÞ¿c<çõKâ¿ë÷òÜ¢]üîÒþübκ@ûÞI„YL‡í·{B8k÷…£¸p+_:€[^YÉ–’zî=·7wMêɯÞßÈÓ_å22k¿8¿7;J“ÇUÿü†£ÿTD¤#Sð9„Ñ`ÀóCyFfÅðÔ—¹/Á_n-ÃpôNŽ`MAM`Ù~;ÊêÉŒs†cå–5`1IŒ´±·Ö À¢å’[ÞHC‹‡ÕÕ4»½V7“í`kI=>¿Ÿ©CÓK¸ÍL´ÃBv|X›l- Ô·¥¤ž´èÖ9½’"0`In>¿ŸÏ6•pݨ̣>ßúßæW­=?ÛKëI‹vó5ry}¼°4¿Í²F8ÆÐŒhÜ^ƒÓ£œ €ÕldHzëܯÏÏïnàÝŸŒÅf6òã×׆]‰ÈéGÁCDDä{ª›¹ ÿ±¯¼a3Óä:0<É4¹¼DØüiuº¬÷ùÛ>öúýøý`6¸JÖÁó7<>_`®C`Ÿ?Ðþ®I½Ë Kó(«k¡oJ$÷Ÿß§Mûƒ·÷úü˜ömn5µ©}µKó!ëÝ^›ÚÄãóóÍ®Ê6ËÎí›H\¸€h»·ÏO”ÝX_ÕèâÇN·—LJÙh49=)xˆˆˆâóM%üdBwÎê•èØ/)ÒFY} ù•M H ¬OŽ´‘a%¯âصÛËY=ãynÑ.n+`Xæñ] `wU‰6âíT6º€Ö‰Ü'ÊíõBÍ÷‘[ÞH¤ÍÌ[«÷Pïô¶ÞüéŠA|³«Šš&]=„¯¬ÄëÓ`+‘Ó‘‚‡ˆˆÈ!v”5ðÔ—¹<~Í^Z–ÏúÂZ,&#“$f5ó‹9ëxyY>¾r…ÕÍì­ur÷¹½ør[9{ªOMðÈ«lâªáéì©n¦{B8?Óí¸·Ý[ëäËmeüéŠA<½0—î á\¼oû‰È¯l">ÜÆõ£3©jt±lgåwol,®eÙ®JþwúpþïëÔ6»é™A´ÃÂ+ ¸ylYqa<ðÎrÜ^¯ßzwMìÅ“_î8¡:E$´L³fÍšê"äôçñxøãÿÈm·ÝFFFF¨Ë9¢ÖSTs|ÃuÖÔ°±¨–q=â¹`@="ØZZÏÓ_åâòøÈ¯lb{Y—NåœÞ‰¬Ü]Í£_l œ ·²vO-ÕM­_æÃ,&Z¼l(ª #)ÒÆâÜ œnÑv y•MTµ‹ÉˆÙhl3T)6ÌÊÆâ:ÊZX½»šAéÑ\9õÅŠˆˆˆˆtP ßÃÀ8p ¿ûÝïX¾|9o¾ù&o¾ù&555˜ÍfM0é ¶nÝÊòåËC]†tueQ@D¨Ëé’æÎ~‘Gn›Êƒ×Là÷·^Îëÿ·«å¸¶_ùå'T–·Y¶ì³w©¯©j³ìWÓ&RUº·=J‘S¨]‚Çço>Ïü9/qÅm÷ñà?Þdú=¿ÁbµwïÄG/>Eaî–6Ëf?ýG*öîi³ìŽGþ—ÈØøö(YDDDDDN¡v¹ªÕ†å_1á²ë<öÒ2é3ôŒ6mªJ÷²|îT——Ýoã/ºƒÑHÎâyÔ×V±ê«Ï(ÊÛNfÏþ45Ôánq²ì³÷زz½¤Ï°3(/*Àçõ°yÕR µ·eí¢¹DÇ'rÞµ·8榋YÿÍBÂ"¢8ëÒi|;ïC.œ~;&³.æ%""""r*µKGlb*kÍcOîÖ#®ß“»…Gn»gSÙý†°ì³wyá‘û°:Â0MØá„GFcµ;°‡…ìa­Ë,6ï<÷WëjX·d¯ýí!ý÷-ºõÈöu+yzæíc.ÿ➟u‰iÝŠá™_ÿ”wž}¯ÇÝOYDDDDD¾‡v9õÍÏfòâ`ÖŒ)„GÇÒäx.˜öCz  À;ÿ÷(L»•‹oº€Q“¦ðÀã(ݓǀQgÍ 1g3ôÌóû´ØlŒ8çBº÷zÔã†EFsÇ#ÿ Àð³'sÏÅ#h¬«!<*†^zŠëïýc'O "*†ç¾·=ž®ˆt`>ŸÉ“'SZZXVSSC]]ƒ,3™L<ñÄLš4)eŠˆˆt9ílvî–ã»F= gõÂÏËV-üô„ö%"§ŸI“&wÔõ~¿Ÿ›nºéV$"""íÒãñÿïçô4»#ŒM+—ÐXWÄˮຟÿ»ûê*Ëé9xõÕU¬[ö%<ù1qô1žþõwrׯ¦Ï°ÑŒœ8…~#Çñæ“ÓoÄX9‡AcÎþ^5]w×C<ñÀÉߺ»uR¹Ñdj§,"˜Ñh䦛nâÙgŸ¥å'0^xa*éº ~¿ßð‚%¹•ì(oø^;ij¨c˪¥çíÀëõ’”Þ‘§`s„Ú8›Y½ð3J‹ò Œ¡Ï°Ñd÷‚Á`Àï÷³cÝJÊ‹÷šAßácðù¼l]³œªÒbÒ{ô¡{ÿ¡|ýÑsþåØÃÂÉݰƒÁʰøã·9ñ¢À%uëkªØ±~Õ¾«eÙyê[yò“ÕßûEºbh±a–h„ÎÊ•+9ãŒ3[n±X˜1cÿú׿BP•ˆˆH×Õ.Á£#ª./¡¡¶šŒ}©«®à…Gî'!5“3ÿô½÷¥à!rzÊÎÎf÷îÝm–F,XÀĉCS”ˆˆHÕ.s<:"gcÿþã/¹{Êpþpû•$¤f2íÎ_‡º,9…f̘Ýno³,..Ž &„¨"‘®«Óöx´'õxˆœž¶nÝJÿþým6wÞy'?þx«éš:m‡ˆH¿~ý0`@àqKK 7ÜpC+麗˅ÉdâÖ[o u9"""]V»Þ¹\D:Ÿ²úÜ^_¨Ë8aÆÈ†Íš•+˜xñTŠjšC]ÒIIŒ´a5uÍsF5Ín[<¡.CºƒÒ¢¡.㸹¼>ÊëOìæË"í-Üf&ÆÑvŽ´‚G¸\.¬Vk¨Ëi3^YÅîÊÆP—qRª“ÇbO©ägŸ•Ãgå¡.礼që ÏŒ u!ñ÷ùÛygMQ¨Ë.ÄlúÝäP—qÜ6×ñƒ¯u"\9,?NÔf™‚G;illäÃ?äÕW_Åh4òé§Ÿ†º$‘váõù¿»Q5ðlðyC]†´üœþïI‘®HÁã$¸\.¾øâ ^{í5>úè#|>‡³Ï>;Ô¥‰ÈALŽHbF]ê2¤ àWî9-)x|O>ŸeË–ñöÛoóÚk¯Q__Á`ÀãѸc‘ŽÌ`4…º‘.MÁã8mÚ´‰9sæð¯ý‹½{÷b6›q»Ý¡.KDDDDä´ àq {wïäÛyñ»ùï³§ »ÝŽÓé8fè(,,dÖ¬Y§¨Ê®¡gÏžÜtÓM¡.CDDDDN‚ÇQ¸]-l[³œÍ+—P¸g‹%:¾K]]_}õUp ìBòòòèÓ§‚‡ˆˆˆÈiLÁã(,V¯¼‘‰WÞȘD˜÷ɼþúë¬ZµªMÏÇ‘ 0@Á£=ôÐC¬X¡ËŠˆˆˆœÎºæ]¨¾§Ô´4î¹çV®\I^^³fÍ";;›ÍÚâDDDDDN ßSvv63gÎ$//•+Wrçw’””„ÑhÔMEDDDDŽBÁã$Œ5ŠÇœ½{÷²`Ánºé&"##1›5‚MDDDDä`Gü†lŒFÃ).¥òƒ÷8îTe4™8q"'NäÙgŸeË–-§ 8‘ÓÇaÁ£OrÉÑš·p°pëñßxÌjµ2tèÐ V#""""rú9,x$EÚHŠTð‘ö£9"""""t """""t "ÒîLF©Ñv¢–P—TÑ WKur 6³‘ÌXvËñÏÕ;õHgB¯„Sv<£ÁÀ´‘„}9Ò1™ŒÒ¢DÙ;÷ïëØ0 S‡¦ÒcNîŸLzŒã”³£Óu_E¤Ý˜îšÔ‹é£3py|´x¼üsQsÖ†¸ºö—iã¡)ýx?§€\7Œ=ÕMümîöïÜ6.ÌÊÿ»¤?÷ÍYÇw_;ïÄ•””’’Ä#tLqáV~3¥çõK¢ÎéÁj6²»²‘Çæí`E~U¨Ëkw£³ã8¯o"‹s+XòˉÄ8,œÿäbJêœv³.À´‘üñ³­¼±¢à„g4´îkÑŽ š\ÞÃÖ?yíP&HæÁ÷6ðñ†½å—Nå¯W fîæRî³î„pç9=Yµ»šo;ØÏótùÌYLFî=·׎ÌÀï·×G“ÛËÿ}½“÷sŠC]^»K‹v0ó¾|¸®õ¹=wÃÎîÀÏgçðå¶²@»kF¤óûËòÁºbþçƒ'uÌŸžÝƒg¿ÞIQMóaën;³;÷ß§ÇGUc ÿŸ½ûŒ²¾8þ~n_î.{„°!A@EAPq€âÞ£¶¶µîÑjíÏQg[·ÖVëXL‚2dC „!{ï\Æí»ß. $¬Âø¼þÑܳ>÷ð}žû~žïx¶50ã—2Kþ§ãžÈ$ñB3ÏNBzR·Oß@vy#ƒã‚¹itÒ)™xü/ Z5ŽAQ|‡1m÷‘¨««cΜ9LŸ> 6`·Û½Ñ)Ä QñÑm£(©·qÑ›+©lt R.ŽBOÉÄ£3Eu6¦¦ÅñþÊ<À^& Ž¡¸“JPw(¨mኴø‰Ç•éñÔ¶“ýJ£Ìj‡üc²»ÿIQQ_}õü1ƒM›6õtH‡ôÒ•CI6sóGëÙSÙÀ°„®‘pJ&)¨máÊôø‰Ç•é ǬŒÊæ¢znùh=½†„0#—eæ¯Fsï—[N5’x!މ~Qf®L‹çÖéëI@V™•¿|¿#ð÷˜¾üêœd¢-zv–[ymé*Ü~? j[Ý;Œ½BÙ]ÑÄ3ó²›Áíc’ñú|üsÅÞÀ yZzž2€n‰ÏçãÝŸrÙTXwÔç¿¥¥…¹sçòÉ'Ÿ°xñbT*.— µúôë 35-žØ`=·}¼«ÝÿoëõùXš]ÉÒlCnÄ¥Ãâ0hT¬É­áŸ+öâp{xç†tfm*æ¦Ñ½H 7²|Woþ˜ÃÝãú0yp eV/.ÜI~¿‚òؤþd•Z—ɰ„v”ZynþN&Šá–³záp{xsYNàéüÐø`~}nR¢L´8=,ÛUɇ«óñxýIè?®μme\5"ž”(3Åõ¼¼h7Qf=ºxc-d–6[Õ|Àyøvk)W¦%ðŸ•yø€‰ƒbÈ(®ïÐÒ UóÈÄTÒC1hUd•YycYN •¤o¤‰/L%%Ê„Ýåå—Ü^Yr`‹žI§áÉK²>¯–o[Ÿ(/ßUÅÔ´8b‚ TXíÄkaî¶2â‚ mo?»7Œ&ܤ£¤ÞÆ¿ÎeKQ=f½†‡'¦2²WйÕÍüßw;˜2<ŽA±"M:.KFqo-ÏA£RøÍØ>œ—…Z¥°tg®ñŸ×ø#ÿwÙ@f®/â·ãú`sz¸{ææ£+d@MM ³gÏfÆŒ¬[·½^ÝngèСG½ÏãeH\0— ‰åúÖ’€í% l/i{Ú>®_$·éM¤YÏŽR+¯-ÝMM³€G&ögWE#ç¤D–ÂβFžŸÅ…¢¹å¬^¸<>ÞZžÃ/¹5\72£VM˜IÇÄÑ”5Øy~ÁNb‚ Ü;!³^׋˜µÉÿ*Ú¢çþóû1$>€u¼µ<‡F»ÿøÃ„Šël Oá¬>áÖ¶ðò»(lMôOìϘ¾á”5Ø™»­-ÞgqV×L$˜Çþ»E;*xõÚáè5þ²nЪ™³µ„ß|¶™çdsÑ n9³W`#{…òðÄT¾ØPÄC³3HŽ0ñ»ñþò¦ÿºiv¿¹™å»ª¸ãœä¾ãŽ2+N—ô¤PÀßÚ°ÿ“lJaOe³{>ßB}‹‹7¯k{Õ߮ƎR+·}¼‡gg°£ôÀrmÑó飩´:ÝXœ/‹vTpEZW¤Å³0³ç~2›ËË‹²¹ëÓM,ÚQλ7Ž Ü¤àžñ)D˜tÜóÅ~óÙfd–° ³œ¼ê¾Ë(ã™yY‡ ÏNBZb(OÍÝÁŸ¿Ídl¿H~ßzujÆö‹ä–3{ñ·vóN¨C±ÙlÌ;—iÓ¦Ë<Àúõëñù|'UËâÙ}Ã)³ÚÚ¥çŒ^¡¼~m³7sß—[ñx}||û(Ô­/˜ç¿_/Ë®äþ¯2ˆ 1ðŸ[F’–èb÷ê5Ã1´–ù^áAüaB V;÷}¹•ÊFo]ŸÎmg÷â……Ù¼¾lOLHßHà¿FÖæÕrÿW<<{‘f=ÿwé @|b,k-k­É'-1•ÒVz¾ÝZÊò]UäT51{S IaA¼´È_^>][€A£¦Wxàz»zo ªÖ®vKvVtøq˜þKkójÙSÙÄçë 9#) €´ÄPÃŒ¼üÃ.JêmüUÁ²N*;þ˜J¸2=ž¸kÊ]U–79ÜÌÚTL½Í…I¯aAf9ƒã‚ òWüÃtÔµ8©oqQPÛÂÂå¶ïeâÓ;Ïä«MżñãžÆ+}»µ„+ÓPð'>ßvÒ…gÖ¦bŠjmXôvW4QRocToÿw 7ii°¹©kqRaµ³8«‚F‡›z› »ÛCm‹“¢:ÕMb,z¦ãOßlgOe9UMü}ñn®>#1p,µ¢ðÂÂd–6txÒ0íüððp®¾új¾ÿþû@‚ïõz½“ÌáܯoÝ‹9[Jø!«‚¢º^X¸“H³ž³û„Ö™»­ŒeÙ•ì­jbÖÆb’#‚x¡õùùúBÔŠBrdÛýzkQ=_l("¯¦™¿ä“mæ‹w³«¢‘•9Õdד–è¿GÖ¶0{·ƒVżíeŒïÕ!ÆÅYÌÛ^F~M ï¯Ì \“Š+Óãy~A6ù5-l.ªgÆ/~Oÿ5’€ZQ˜š×iýrc¥õþ2º£ÔJ]³“´ÄÖs©¥Á梮ÅEYƒ™åZ;Bƒ´|xÛ(jšœÜÿÕV쇸ח5Ø[÷«;èzí½»b/ÛKØ\XÏO{ª¨·¹˜½¹˜¼šf>Y[øÝÙç«Å¬Ê©fwes¶”eÖóê’ÝäÕ4óÁª<¢ƒõÄðú|ÌÚXÌ5#Û®¡kG&0kSÑaǶ?éj%„8&îÀSÊ®$…YÔ®ââöúÈ­n&©µ2t„×dwQÙèÀéi»‰[í.,íf_i¿þ¾n(>³»6øou±ÁÞ¹!­ZEYƒ—Ç‹A£ÂlКï‹êÚ¶µÚ]¨“NMNƒN­boU[eåp*.Eum}…­67QQG÷‚V{YÖí˹øŸ·R[]…V«Åétr;¯×ËwÜqTÇ<Ñ4FóðC®×tˆ²¨RBìªhë˜WÝŒÛë%1Ìèz×¾YínJêÛžh7ÙÝhÕ* ZU`puû±M7V{àÉ©ù´´–ųú„óâC©h´S×ì"Ĩ ,Û§¸}Ù±·m›f¤°Öèþ²ÓÉËçn+cîÎ¥®Ùåomðt¬$[ôÞº!Ø`Eµ-4;=ø€H³¿ëÉ‹‹²ùË¥ƒ¸ÿ‚~¬Ù[Ã'k Èl×êñZëñ¯6v^É,µâòxùÕ¹É8Ü^v”Y™48&°\þ|É@& Ž!§² «ÝM¤YO”Ùÿ]Þ_™Çß®ƪG'°± Ž9[K;ÜCÚKŽ4³~s6zÿßFQÌ: šÖ§ô.—âºÃãâóÑ’¿†m?úÊÕØl6E9¬Ä¾¢¢¢Ç®¹šf'ލñècút½F»;\v%)ÌÈÚ¼šÀß·—üšf’‚ÿçï¿.*¬Üʼ«ÃlYݯ‹;ܯÝûuB¨‘wnH ÜjÇëõ—WƒF¨Øïw¿ÞwMF[ô¨ÿu½ON÷ëu˜õî8§7V¹Õ»-ª…§/ÄøÔ(rªšh´» ÒÕz½½ûS./OÊêÇ&°>¿–¯7—txðÇIÈ*³òÜ‚Qf]àû®ýÏa³ÃÝáo³¾ã½¥d¿{UI½-ðÐÀíõaszü÷›˜³¥„{Æ÷%6Ø@X–äS åñhHâ!„8&6ÖñÈÄTR¢Ì*çíYí.‚÷›b7ÄèZ´Ïþ-Їê-äídyWÛÜ=®ÛKxv¾ÿ WxŒFi׿áëbŽ©}?ƒG“ÿIááL?yšövêQ› ë¹õ¬^è5ª•ó}¼>w‡±A: Zµj¿²ØñïP]×(»Y÷ñ‹ðú²=×ÓÒã¹s¿îR]mom—Lï³ÿßûÔ4;ÙRTÏ]ç&sÃëX~ãè$ì.—¼½ ð@ßøç QZ[r–eWòcv%c-LM‹ç“;F3ñ•ëá/ßíà©Ë±£ÌÚeeäÛ­¥<<1•W—ص)=)”‹Ç0ùíUî‹»Îb_CRnu3×¾¿–¸“Åðì”Á4;ܼm°¹p¹½\úΪºÊìãñüßåt±¥¨ž{'¤Ð;<¨ËÔ v÷÷¸`Ã!îׇ8n§ÿ,]lôûóú²>¿Ž—eþq„çˆå0î×6·ÿ¡‘^(«ûÿö´?üw¥‰U©TLŸ>ý¨ã<‘<5w»¶z¶ÿn.á¶³{óÇIxù‡]¸ZŸò÷6“–ÊìÍÅü’[Ã5g$°!¿þ¯µ-”ÖŸ~úƒ&ðTR«VqÕˆÃ̶â"Ì:Fö cSaF­šÉCb)íb¶ª7–íaéÎ vt2ލ}kÀµ#;t‹¶è©lt°³¼‘¼šnDh6P™ÛVÒÀ¯?ÛÄû7Ÿ^£êt6¤o¶–Ðìt³8«â€e&½§ÇH‡Ä34!˜ù™eŽ_Ö`gÆÚ.C\ˆ`zƒÍÕá©ýÞªf*Ü4:‰O×µM¼oGDQ꓆©OÛþ2åË—óñÇ3gÎ<§Ë.V111=vÍí›%éP6äײ¥¨žg¦ æ¯2ÿž‘f=WHཕ¹ü’[ÃÔáq|¾¾»ÛËèÞaćÙü?L‚q$ÌzM‡qGr¿®mqú‚ôxf¬-8äýú«ET79:Mž-z ·§Ç_=?£W(©QæÀò}å«¸ÞÆGkò¹lXñ!FÀž*Ü6}Þ:ƒVÍË‹²;­è›õ.ËÝãúò켬À:Åu6ÒC(ªkA¯Q1yH öãÛ¥öË E¼zm&š[?Þð?íK!Ä1óÐì ž›:„ÏÞª&n/)‘¦ÀÀÏY›Š9«O8ßßs{«šIK á¥E»:ã¼;|µ±˜Ý4‚þ1fBŒZöv2ÐÁüuþNþ}“°»Y¯¦°öèãnvº™³¥„¹÷œCq½W–ìf]Þ¡§yU©TŒ;–±cÇòúë¯*Dß|ó N§ó°»ƒœÊ¬v¿þtoí¢³§ª £VM¤YÇ‹ ýOO_Y²›Ýt³ï>›F»›”(În›¹¥»}²¶—§ c]~-}#Md•Yû…›õ6/.ÌæÝG°©°Ž¤0#yÕÍ ¶·§²©Ënßn-å³_ÉÇ·BQüûnv¶•Ÿ÷Ž%»¢‘ªFc-,ÜQN^us`€1@vy#w~²‰n‰^£æËýº]ÕÛ\™Šö·!¿–ú_ß}6e v¢-z2KÚ¤ç§!:Ø@AM3Q=šZ˜9[JøÛ´aLÇ/¹5¼üÃ.ýï6þqõp¦ §Âj§WxEu-ÜûåÖÃ:·Q«ÕLœ8‘‰'b³ÙXºt)}ôóæÍC¥Ráv»Oºq>àþ¯¶òÂCYñÈxr*›py¼ô41c­,Äçë Ý;Œïî9—üšf†'„ðü‚G=¨øH}¹±ˆ·®OgH\0á&-{*ì~ýì¼üóÆŒK$Ĩ¥ ¦ë)rkš]–ÑU{«¹÷üfß}6U‚´ì,o+£¯¶NxRT×Bl°·ÇÛaz^€Úf'·OßÈn9ƒ§/Ì_[[݇ÄóÃýã0éÕ„´l/mà±ÿnë0@ýß?çòÌ價¢u¶¾Ý•Mº›‹ê©oqRaõvè¢z4ßé:í‰8i<ù䓬_¿ž%K–ôt(§¥Io­êÐ×üpD[ôô‰4aszÈ«nôåݧwxÑ=9UMÔµ´5ÛGšõØ]šZ×7hT„é:¼-6ØàXêòbÔ¢Vj[ücÔŠB|¨âº¶þªá&.7ðT7ܤ#5ÚLU£ƒüšB ”ÔÛñú|¬ë ` ´Þ¨štÅY¨irR\o#Ú¢$N&¯z›«ÓX,wž}Ó"‚j“Žê&ça 0Ÿù«3(ŸN×å:ü¾Â'²§æî`Ζ҃γ¿¾‘&b‚ Ô6;É«nî0ÆA­RèmF§Q±»¢©ÃùO5RÕn|Q°A‹Fu`YÛWv¢-zšž@¥Ý¨UlèøÄ6.ÄǾ§ûIaA$†ÙSÙDSkׯŠÖ²¾ÿº­šP£¶ÃµeÖ“ebO¥?É7hÕÖ³ø#µ-ÎN²F[ô´8Û®3‹AàØ`¬vÙå$„©ltàòxÑkTô±`ÖkZ'Bðß ücMJÚÆ±X B ZŠëmD˜t¸½¾Ýrö 1úÏå¾iY5*…a !(ÀöRæpyhtø»Ëô4eÑÓ`óÇ×þß_£Rˆ0ëñx}ï®Q)¤D™ òŸÏ}SkÕ*b,ú#z‰ìxzR§Ë6îöíÛûÇÒá¶x´Óz¿nn½_7µ»_+@ïˆ ¢Ìþ oûÏ(³¿í+ó­ºC9®kvbw{ 1jQ)îùþ7¦:Œ«‹0épº½ßŒ“Ž~Ñf*¬vŠjmî©û¯»ÿ5 þV„A±–@«Y¤YOiƒÿx‘f=·§C‹ß>aAZÚ®w­ZŰ„`|>+_x.ðÝÕŠBJ”‰³žº'»Ê÷ü˜`V›+po1hTDYôTX赪Àd·—ú×c°ÚŸÇäÙåV\F­šªÖòžf¤¬Á[³oŸûzhT qíÎsŒEÕîÄdÒi0éÕZÛßö•ƒÿþv Ÿ®+ ¼0÷pLKç…+:N/-‰‡8áIâѳŽ&ñݧ«Ä£½Sõ‚G“xñ¿8XâÑÞ‰òÁ£I<„8˜>&.Ë5g$rñ›+»LŽ:ÓYâ!Óé !Ä)&,,Œ»îº‹•+W’ŸŸßÓáqÊKJJâÑGeÇŽÌŸ?¿§Ã☹nT"©QfîûrË%]‘1Bq ‹íé„8­È5'N%ûa×1ÝŸ´x!„B!º$B!„Bˆn'‰‡B!„¢ÛÉSŒÇëãûme=Æ1Uå5¡ëôÅTÇË´ÿáEqB!„BSŽÝíáÉï2ѪU((‡Þàd`: ÒÎâÙy;û¡½>/n¯ïˆ††ÊËË0`@7F&„Bqr‘Äãå:Sž‰Ãg·Û™?>Ÿ~ú) .äÑGå…^èé°„B!N’xq”<Ë–-ãÓO?eΜ98ÎCo$„Bqš’ÄCˆ#´iÓ&f̘ÁÌ™3©««C«ÕvH:ôz}F'„Bqb’ÄCˆÃ°cÇfÏžÍÇLQQQ‡dCZ:„B!M!ºàin nó¶.eè3ÅhµZ\.pèdã³Ï>cõêÕÇ#ÌnWx.¤Žïé0„Bq’“ÄCˆ.¨ƒ‚1%§án¬ÅämÆjµ¢Óé«…£wïÞL˜0¡ûƒ<æ×FÒÜÓA!„â¤'‰‡]QŒIƒ1& &óÿ¾aÍš5LŸ>/¾ø‡Ã¢(¸ÝîN77nÏ<óÌñ·›¬ykÍu-=†B!Nròær!ƒJ¥bìØ±|ðÁÔÖÖòÍ7ßpÝuס×ëÑjµ¨Tr) !„BŒÔ–„8Bz½ž)S¦0sæL***xÿý÷?~<*• ½^"„B!ÚHâ!Äÿ $$„;ü‘ÒÒR^yåFŽÙÓa qÊòùz:!„GKÆxqŒÄÄÄpï½÷rï½÷žRSìªUJO‡ D€É<„âd¥ø|òüèTÒìt3ú¥{:ŒSNÖÓ“z:„SÙèÀåñöt¢U”EN}z6V×Û\4;:ŸÐAˆî (bìé0›Ó㥪ÑÑÓa€I¯!Ô¨íð™´x!*Ú"ob'†P£ö€1!DZEBèÉ“(‰ÓÏéùØL!„Bq\Iâ!„B!„èvÒÕJ`1hÙ+Œ° ¥õ62Šë±»OÍ~ýÓÒã©oq±|wUO‡"„BqÚÄC0-='& ¿¦…¼êf¢ƒõô4ñЬ 6Õ÷txÇܨÞá”6Ø$ñB!„8Ž$ñ8Í•Î_§æ_láç=Õχ%„0sŽE¯Ááöâìb†#£VA«¢®Åÿ=µ¢¤¥¶ÙÙé˜:µŠ`£–š&G`y¸IGƒÍ…ÇÛq µ¢fÒQßâÄíí|"6½F…Q«¦Þvà üÔ*…£?!„BqüIâqš»óœd–dUtH:¶—4þ¿wx¿j aF´*«÷VóÔÜ,šn’#‚øäŽÑ|±¡ˆëG%lаpGŸ¬-àÕk†lÐàôxùõ§›È¯iàçGÆóÙúBnÝ‹ ­š=UM<>g;/MJRXF­š‡¿Î`õÞîŸÂMg&ÑìpnÒ1w[Ï/ÈÆëóeÖ3÷çðïŸs¹óœd Z5ÙåüþóÍ´8=\?*‰G&¦R×⢲ÑNƒÍEiB!„â8’Áå§¹a !l*ìº;•üýªal+i`Ü?V0ᵟˆ¶¸ïü~¨…H³­ZÅ„×~ââ·V1ip4O_>ˆÛ¦oà¼Wb]^-wëاŠeH\0“Þ\ÉøWW`Ök˜qÇhž›¿“ ¯ýÄÛ+rxô¢þõgU0áUÿ¾/|}%#’B™48¦õølÐ’jäü×~fü«?aÑk¸2-€~Qfþ4©?·MßÀÅo­ä­s˜Ð?ªΤB!„8IZ“××qжJQ¨ht`Шˆ0é¨imÁH 5Riut:`üX‹0é¸xp g¾üc ¹k9ìí+­vÒC;|j ¯¦ù˜Æ)„B!NºZæÞû9—° -Ï_1„äˆ TŠB”YÏc“úséÐX*¬vÖç×òðÄT4*…° CÉÈ IDAT-¿ׇ﷕—øœ/>ôö'g&‡sáÀèÃÞ~QVcR"Õ; € D3,!¤[bB!„]“Ó\U“ƒ›>\ÏC¦òõÝc0jÕÔ4;Xš]ɺ¼ZþôM&Ï^>˜ÕÇçcAf9ïýœ €Ë㣸ÎÖqŸšîÀß-.V{àï’z.O[{IƒÍÕaŠ^×GQ |Ðhwóâ¢lÞ»ù ¼>™¥V>ù¥UkÊìn]××®µÆjsahíÚUXÛ“ßfòÊÕÃ1jÕl,¨cÖÆb¬×"„B!Ž=Åçó3â8ivºýÒG½½V­ÂÕÅ{:ÔŠ‚§‹‹F¥tùán3r4²žžtÔÇB!„ÒÕJì§«¤èѤøŸ’ਓ!„Bñ¿“ÄC!„BÑí$ñB!„Bt;I<„B!„ÝN!„B!D·“ÄCˆc,''‡5kÖôtB!„'y‡Ç@ii)³fÍbúôédddðÈ#pÎ9çôtXGdeN5 3Ë{: q"Ízž˜ÚÓaœÐ¾Í(e}뻉ÄéE­RxnêžC$Bµºº:æÌ™ÃŒ3Xµj:‡ÃFsr^V»+š˜»½”ƒÌ¨,NP1ÁI<akQ=ße”"“jŸ~TŠ$Bœ(NÎ’=Än·³dÉ>ÿüsæÌ™ƒÏçÃãñàóùp8¨T'oFJÇëéé0„芢 ïÌBˆž#‰‡‡âõ²jÕ*¦OŸÎ矎ÓéDQÜnw«{Y½zõqR!„âÄ&‰‡]ð´4Pµâ3¬Û~dÜ_›P©Tx½‡î‡äv»Y¿~=ÉÉÉÝä1dÃxõó=†B!NQ’xÑuP‘ãn@OlÙ/ddd`0°Ûí‡ÜV¯×óÌ3ÏtÇІR;k¤ŠB!º‰$B„ÆAøÙÓØúô¿(((à£>â½÷Þ£¢¢â Ûét:î¸ãŽãä1âYÏš{{: !„Bœ¢$ñâÜ5¼þúë̘1#Ðêq(>Ÿüüüîî(ÄÄÄ`4{: !„Bœf$ñ¢+^/Õ«¾¤zÅg<Ün¦§ÃéjÕÜÜLŸ>}º3º£¶`Á.¹ä’žC!„§I<„èŠJEäy7:âb~ŸPÞ¡ÅãPɇÙlfëÖ­Ç)ÐÃ7pàÀžA!„§)I<„8%‚‡º‘‡zˆ‚‚¾ýö[Þ{ï=vî܉N§Ãét°Mxxø 9«•¢(=‚8ø|>***ˆíéP„BœFNÞ7 Ñz÷îÍ<@VV[¶lá &&EQÐét=ž•™™É“O>Ibb"O?ýtO‡#„â4#-§•¢c1àöʼ¨ÇŠVÝy+Azz:ééé¼üòˬ^½šÏ?ÿœÏ?ÿ«ÕzR¿½\œZòóóùâ‹/˜>}:»wïF¯×wÚJ'„Bt7I·“¦=¨ÊZNèŸü­l¯×‹Çã9è¶+V¬8é^rù¿(p[ ÿÔžC!NkÒÞ.„B!„èvÒâ!„8jgô åÂÑ„é(o°óóžj¶×÷tXÝâžñ)x¼>Þ[™‹A«æ–3{–9Üv”YÙ\xl¾ûˆ¤Pî¿ wÎØxÐõË séwÖ…Ìÿí¨.»Zuf„ ¼÷Þ{Ç$Þ“Á3ó²øzsÉa­lÐ25-Ž1n/»*™·½ŒçÁ[‘NFaAZ¾üõÙL}w5·—±ý"c ,¯jrðã®Jíîcr¼·®Oç¿›KøiO ¡F& Šáã_òɾ…'>iñB•'/È?oJQØZTÝíáïWãÊ´øž­[„µ„µiÕ<<1•¤p#ÁF }#M¼sÛÔÿ˜K¯Ql8¢m‚‚‚¸öÚkY´h•••¼ÿþûŒ;EQÐëõÇ$®ÓÁ€ óï=—óR£È­nfWE#gõ çë»ÇôthÝB¥($…Ù7wßÄÑLG°QCX–iéñ,¼o,±GX»mѤSÐ+<ˆû/èwLö+„89H‹‡âˆ]2$–ëF&2íß¿[ÝøüÓµ…Ä…´UP‚´ OÅíõ’QÜ@“ÃÿÔT£Rc!»¼‘ÑÉaµ¬Ù[C“ÃM|ˆ‘´ÄöT6‘Sը׀ …µ-¤D™I 3²¹°ŽŠF!F-gõ §ºÑÁ梶µ¢0(.˜¤0#u-N¶Õãp{ËÆZÈ®h$=1”p“Ž­EõT5u„=,!„„P#› ê:=Ó×Wãÿþëòëøë”ÁücñîÀò šôÄPtÛŠ¨mé8m|ˆ‘ÁqêZ\d׳i°ÃÃùí¶Û¸í¶Ûd:Ý# V^»v8?í®â/ßï|>kS1Cãƒ+Àø`âCì­jfo»ršjÄåñ¢( g$…RXÛÂŽ2+j•˜>áhÔ*ÖæÖ`o-‹á&&†z›“1}#h°¹XŸW‹8#)”³žµÔµ¸Lj0éž‚F­"»¼‘¢º–À²Ø`>Àåñrfr8•öZâƒt¤'…Òäpw¸~÷É*³òÚÒ=€?1™÷‡s¹p`43×Öée¦o”‰Òz™¥ÖÛëÔ*ÒC6jÉ,µRaµÖùO 3bsyѨF$…RTרw„I‡Å !¿¦í»ªU c,ì®lÂåñÖ1„=K!Ä»jDó¶—Pi±¹<ÏÎLçõkÓØTX‡A«¦_”‰ßÍÜÌîÊ&"L:fß}6K³+ˆ4ëxdb*¯/ËáWç$SPÛÂ_§æ‰o2ë|xëH2JÒª±¹<<;e0“É=ãû’[ÝÌÙ}Âùjc1ÿüi/O@¿(3Åõ6’#‚7é¸éÃõ4Ø\´jfß}6KvV Ó¨PPx~ênø`] ‘xæòÁŒíÁº¼Z~;®ÍNÛŠº<'áAÚ•þ‘&Þ¿å jZht¸yéÊ¡<úßm¬Þ[Àu#yèÂTVæTÓ'Ò„ÏçãןnÆjwuuˆ£’œœÌO<ÁOO_>ˆw–ïåËEÜun2ý¢Í´ì­jâÌäpgUðâ¢lÅZøÏ­#ÙRX¢€V}ðŽzŠ šºvIó' à²a±ü’[CZb(9•M<8+ÏG„IÇ·ŽÄáöRTgãù©Cxù‡]|—QzÈóÿðÄþ˜ôƒ´äV73¶_$Ó×äóþª<’‚ø÷Í#ÿêOç¦DðÌ僙øÆÊCî[qbÄCqÄR¢L¬Ê©îr¹<;e0ï­Ì哵<1y Oȯ>i«Ôý˜]É·¥¨…ù÷žËmg÷âÆ×áñú¸õ¬^ÜrV¯@âP\gã¥Ö Ôצñ×)ƒ¹ìŸ«i°¹Õ;ŒÞ0"xü}ñîOA_½f8×LäýUyÏ6Ôñé:ÿSÜW®ÎÕg$ðʒݤ'†réÐX&¿½ŠÚf'!F-‹î{@âñüC°»ÝĦÂ:ÔŠÂG·â7cûðêÒÝt—¡C‡ò /ðüóÏSQQÑmÇ9Y¥D™©·¹¨lìzúá©iñô13õÝ5Ø\âB Ì¿w,‹wVÑZ>ÂM:®}-N—ié ¼u$*•Bj”™Å;+ø!Ë_^ÎH å†Q‰\òÎj*¬vL: sÿpS†ÇñmF)¿ŸB…ÕÁ=_lÁëó1®_$¯^3œ»«h°:© ÒrÃëp{}ŒéÁ«× çýUyl-®§¦ÙÉù¢Y´£€+Óâù>£,p.„'>ã!„8bj•rÐnA‘f=½Ãƒø¾ÝSÎï3J9£W(*¥íMð?îªÀëó±·ª‰•{ªñ´î7§ª™¸c‡ýþØ. ÙSÙÄæ¢ú@e&§² ‹AƒYïžbÔªùõ¹}øÛ´a¼~m)QfúDš:ì¯}R“]ÞH|k7±‘½CY›WKm³ÿ)oƒÍh©hïëÍÅ|°:Ÿ÷Væ²½¤‡'¦¢Vù¿ßÈ^aÌÝVÖîû—b$>ÔÀÐø`ª›l*ôwáòø|Ìß^ƨÞa]žÓcIQbcc˱N&*܇è²3ªw(KvVbsùš—5ØÙXP×áßnUN5ÎÖýäT5áôxYÓZ~œ/…µ-²°½¤!ÐÍoOeŠËwW–ï©l$¾õZðx}œ™ÆS— âõkÓøýøL: Q–¶q\•Ç¿~Îå¢A1ŒéÀÈÞa¬Ë¯ tŸjvºYº³2ðýGõcÞö¶d`eN5.±–ŽÓ™»«÷–ìr+¡F-F­LÈw[KcÈ‚ ZÎÍ·‡7a€âÄ -Bˆ#VXÛBrDP—ËMzE¡ý,@-NZ• ¦íy‡ÝÕ¶Üíõú½ƒ¿‚¥Qµ%)@ 2þÊzûí÷%,û*þÿ¾yyÕÍÌÙZB“ÝÍU#7é:ìoÿíU­ÛštlûÍ`ÔþØûl-jtÍúyO5ž¸€sS"XSƒQ«îðýí.Ÿ“^ƒI¯9`†¤§'pÞDÏ(¬³aÒcÑkhtt>‹“I§¡ÜÚ±E¤ÅáÆ¤kû9µíW®níÓt·×‡ZÕþ:èXîýŸu¼6ö­>¡O_>˜W—즠Ö?Þá¢AÑ´mûk|m×’N­ Q©:,ß¿œT5:ø%ן(­Ú[C¤YÇïÎë˪œêN¯f§;ø˜tjšî–ï{ p(ûo pøn[˜B¤YÏ…£É*³vó!„8ñI‹‡âˆ-È,犴ø*òà¸ZZoÇéñ28®m@îø`Ê­ö‹îbÑkHO ååv±.¯–eV"ÍÆÚ•‚Úì÷„öPOlujjEA­(x|>Šêl ŽkÛf`¬|PTk#¿¦…^aF,í*cƒã‚Éëd ¯8~6ä×RÓìàŽs’XÝZ±Î¯iéðïªRÅYŽ[xl¿Hl/cÞö2¶—4øfE9ô†€Ýí¥¢ÑÞ¡,jwvÅ U*ÿù5Í Œ ¦ý‡Ä“ßš€ûÏOÛ>#L:₠ǤlWXí¬Ë¯cÊð8®L‹?¬q#Bˆ‹´x!ŽØ¬MÅŒOâ³;Ï䓵äV7aÒ1mDËwUòņ"f®/äéËñâÂlLz OLåÃÕùÇ%¾f§‡ÊF·ŸÝ›%;+×/’ÑÉá¬Ë«=¬íȪàÁ Syxb* 2Ë™4(†^aAlÈï8»Õä!1Ô4;ÑiT\2$–¢:ë[×ùxM>÷_J}‹‹&‡›'&ä«MÅ4;Ýd–6°­¤—¦ åý•y¤F›¹nd"wÏÜtÌÏEWÊËË¥»Õ~n/þ6“7¯O'Ø am^-Mv7gö ç²Ö1?_n,bîÎá×çöam^ WH@¥(,Î*?.1æU7sÓ™I,Í®D§Vqïù)±‡ãÓµ…üqÒZî×ç€u’#L\72€ÞAÜ0*)0ViÑŽrî;¿OLÈ÷ÛJ97%’½Bù¿ÖYÀ¦ÿ’ÏkפQPÓBq{ÏOaÕÞêNgÏ:ßm-á±I6jY˜y|ιâØQ?óÌ3ÏôtBˆãã…^àúë¯'55õ€e[ŠêYŸ_èêq0>,ÜQN‹ÓÃy©‘\00Š‹žÅYþÁâ>¬Í­E£V¸jD"c-Ì\WÄ×›‹P©BŒZVì©b_)Ø %¯º™¢:Zµ‚Z¥°¶5Yˆ0ëØ_‡µõEfþ™v\ì,oôïS0“ŽŸ÷TãòxY—WËŃc˜<4–2«/7Q×âdG™•âì_ר_ƒÍÅŽ2+n¯e;+9@—‹goUó3Ë)m°±·ª•¢¤Å¬÷÷­·4l(¨ãù;ÝL²Ê¬T7:˜šÏY}"X²³‚wWì t¹Y¼³‚>‘f®L'ʬç¥v±©uÚSF…¢(¬Ë?¼Dɬ×pǘއ\o÷îݼóÎ;ÜyçdddpÝu×ÖþO+vW‘UÖÈ¡Jwa¥;+–ÂÄAѤ%…Ò`sñâÂl¬v7M7?îªbâÀh.Ku³“'¾Í ”Ë£†¢:[ D£V0hÔ¬j7F(,HËÎr+VFšF‡;0k–DYô¬ÜSèzh1h(k°³·ª™eVBŒZ®•DJ¤‰·—ïÅëõ±ºu\Gûu÷‰²èY½·†§‡Œâz|Àu£éeæíå{ñø|¬Ø]…×ç?–N£"Ê¢'Ê¢§ÙáæÍsXÑ:æÄíõ±0³œ3“Ù:<•JáÏßfR\ï¿n‹êlì,·rù°8&ôbKQ=/-Ìt› Ò±£ÌJe£Z…F­âç=Õ­çNK~u3…­÷•¢¤ãç=mã>Šj[H bAfº˜æzŠ¢pÏø”ÃZWѽ_W¯µBœrŒF#sæÌá’K.9`Ù‡«óygÅ^îSïí̧º˜`Ë:¯Óe%%%|õÕWLŸ>íÛ·c0°ÛíL›69sæçH{ξ7—Ë H§•¢ùÔE=†éj%„§œúúz¾ÿþ{fΜÉÒ¥KÑjµ8þÑvûá½ÌM!„8Ö$ñBˆS€ÍfcéÒ¥|ôÑGÌ›7EQp»Ýø|¾@Ò!„Bô$I<„â$åó¸iÎÙHÍ΄ÿù<>Ÿ·»ó©`ÛÛºu+wÜqG÷yœýío#&&¦§ÃBÑ ™NW!ÄIÏf³1cÆ ½²Bˆ!-Bq’RÔÌÎ&eôünt—]­:“žžÎôéÓoÀݨ´´”Y³fõtB!BZ<„â`4™2e ß|ó UUU|ðÁ\tÑE¨T*ôz}O‡'„BHâ!„§šÐÐPn»í6~øá yñÅ6lƒ¡‡£Bqº’ÄC!Na <üðÃlÛ¶]»vñøã“œœŒZ­îéЄBœf$ñBˆÓDÿþýyúé§ÉËËãí·ßîép„Bœf$ñBˆÓPlllO‡ „â4#‰‡B!„¢ÛIâ!„B!„èv’x!„B!º$B!„Bˆn'‰‡B!„¢ÛIâ!„B!„èv’x!„B!º$Bˆ·×ÓÓ!Ñm|>_O‡ „§5MO „81ô13eX|O‡!ŽB¤YßÓ!œðÒ“Bqº½=†èj•ÒÓ!!ZIâ!„`\¿HÆõ‹ìé0„èW¦Åseš$ÖBÑ“¤«•B!„¢ÛIâ!„B!„èv’x!„B!º$B!„Bˆn'‰‡B!„¢ÛIâ!„B!„èv’x!„B!º$B!„Bˆn'‰‡B!„¢ÛIâ!„B!„èv’x!„B!º$B!„Bˆn'‰‡B!„¢ÛIâ!„B!„èv’x!„B!º$B!„Bˆn'‰‡B!„¢ÛIâ!„B!„èv’x!„B!º¦§B!—Íå¡¶ÙyÀçV{࿦zÛñKœ‚ Z5&]O‡!Ä)E!„'eÙ•üqÎö>÷¹$^ÿ¿ù&•®¼"§šQ½ÂøäÎÑ=†§I<„BœT´j—Ç×á3E£Ã2èÜŠHœŠ<^ß¡WBã!„⤢(JO‡ „â(Hâ!„B!„èv’x!„B!º$B!„Bˆn'‰‡B!„¢ÛIâ!„B!„èv’x!„B!º$B!„Bˆn'‰‡B!„¢ÛIâ!„B!„èv’x!„B!º$B!„Bˆn'‰‡B!„¢ÛIâ!„B!„èv’x!„8¥…µÄP+JO‡Ò­&ô¢O„©§ÃBˆ.iz:!„¢;œ™Îã 5ÆLU£ƒ`ƒ–eÙ•ümñ.j›=Þ1wû˜Þ|ŸQF^M3gõ çãÛFuXîöúþÜ’ãÓ´ôx~;®/“ß^u\+„81Iâ!„┓–Âû·œÁ«Kv3kS1·“NÃÝãútJ&ûsy¼Œ{å§žC!$ñBqÊyð‚T–eWòéºÂÀgÍN7¯/Ûø;Ø åቩ¤%†PßââËEüUÀÐønÄ/¹5Ü>¦7ï®ØKVy#OL@¿(3köÖð%»qy¼´¼~ípþ³*{Ƨ¤eÖ¦b¾Þ\Â'õgtr8»+y~A6µ-þ¤çº‘‰\:4–H³žr«WçóKn ý¢ÌÜ3¾/s·•q÷¸¾hÕ ßl-eæú¶ï3¶_$¿Û‡£–9[J:=V»ë€ÏàÆÑI\:,ƒFÅšÜÞ]±»Û ÀÛ×§óõ殕HJ¤‰«ß[‹Z¥pßù)¤'…¢Q)¬É­á‹wmÑóÀ© ‰¦¶ÙÉÇkòY™SÝi<&†Ç&õgd¯PŠêlüwK — ‰åÑÿn๩CX¾«ŠË†Å2 ÆBV™•fSoó‹^ærF¯Pêm.þ»¹„yÛËcá®s“Y–]Éç$SXÛÂçlgP¬…ßO!9"ˆÂÚÞü1‡=•M+>Bˆn"‰‡BˆSŠN­bdï0k­ÌvFþuÓÊ­všAr„‰—§ Åéö²|w¡AZ.ƒ×çãOs¶3ªw¯\3œu|´&Ÿ›‹W¯N~MŸo(B«VÓ7—ÇÇ‹‹²‰0éxû†\00š¯7óÉÚBŸ<€/L婹;‹Ä+KvSÕädDR(o\›ÆÕïýBq½`ƒ† FãñÁSswlà­ëÒØVÒÀö’úE™yóº4þòÝ2ЏëÜdÒBø>£,ðUŠ?¦}*¬vr«›¹ù¬^Ü1¦7“I]‹‹§.Ä_§ás¶þ.j½Âƒøûâ]T79ñø||vÇhöT6ñ§9Ûñú|ŒH À¨UóÅ]g1kS1ïþ´—äˆ þvÕ0~ÿù¶—4pÞŸ¿bzŠ{¾ØJ”EÇsS‡`ÔªËÓCÙ+ŒeSÖ`çÉKòà…©<3/ €·nH§Ñîæ¡Ù$…ñò´a8=^gUbÔrÑ tjÏÎËÂáþÿöî;<®êÎÿøûNF½Z–mÙ–«ÜdÀL7=„ЗHϦÂ/Ù„ØM6¿4–’`CÙd -„n\Á6¶±\%K²U¬>’Fš^ö‘G²lcäÏëyüà™{î½ßseìùÌ9çÞSó\ž­j¡®³ŸGßl`Qif²í“[šÙÛÞËf¦¡ËÇΖÞaAÁl2¸ãéT·õ±º¦ƒuµ],8ß••%<¿½•¿oo¡¹ÇÏÿ±;9bqÉ0¸eyyò×9³ ¸þ¤IÜóê^6îó°·½;ŸÙÁÅÅd9­É}XSÏš½ìnõrrY9.·ým;µýÔwúxrKb„å’ùÅ4uûùíªZšºý¬ÙÛÉã¸|á„×<Óiå¼Ù…üû³;iðøxk7 ‘:èÑ ûY]ÓÁÞö>^¿ÊI‰>—¤³xR6ß{j;õ>VÕtðкz®ò3³˜ nz;;[¼Ôvô󉓧ðĖĨHKo€'67QÕÔËòk!",xˆˆÈ¸ÒŒ‹ÇÉqÙFmS’íä@o0’|oOk?ø¹½/H(:øa¾7¡©ÛŸ|í Dp;†ÿ3Úèº=LóÐöÁ0éŽÁ÷_={:W,*¡¦½Ÿ^˜‰ÙNòÒköô‡è Ö×í'ÏWšÆ¦ýžä¶P4ƾNß°Z"±Wýný°÷  $ËÉîVoò½ºŽ~¨N\ÔnIDAT"±³Óèö'F)ê:û“Û§äºØÓÚG8:<ؔ幘]ìæ_9t»³)1â´qŸgDÛâL¡hŒ–ÞÀà¹ßQ3 ¿†½0éöDŸ'f9ií $§]ìníãÚ!Á££?„70xͦæ¹(ËsqöÌ2‰ã8¬fvèq^I=W‘;ôrÊÔ\^ÞÕvÈ6½þ0CB@†Ã2lúM<~ˆùæÍGx}ÐÔ<7,™Äò»W%ÏùÛë+1†Üò÷pgê „G„žw¾­>o Bæ¾;mf,&Ó°¾Gcƒgïö‡ÈJ~­’uøÃ¬ÙÛÉ×þ¼õˆçöô‡°[L¸l–d Ê=D8í÷?3ƒÁk“á´¼£îáá¨Çækë¹MÝë‘ÔÓT+w~óz-WV–pÚô¼ä{‰Û»Î.r³ý@/6³‰³fæ`5›¸lQ kw§šÛa! G(ËuqÒ”œ£Þ}]Ì-"Í–X±¨4‹©yG÷ uµ\QYÂÁˆsõâ‰4x|ÃFg†·ïbjž‹%eƒõ9,‰+«;8uZ.åéÉmfÃ8d hó©jîå3˦‰õ!מXzT5ìné#‹sÞœÂÄyL—/*aíÞÑf¯ìnãÊÊ2‡L#sXÍGÒDäý§ÿóDDdÜyuw;w<³“Ÿ\>o0B“ÇÏäÜ4Z{ƒÜò¿[ñ"Üþôv~xiUM=LÌN£ÍàÕÌ7ãUͽT·õñ¿Ÿ?™}>J²liì>êýŸÙv€å³ øëN¡¦­¼tÛ°éS‡óó—öðëkñ—Ï-ň0-ßÅ×ÿò6ÑQ†ZzÜþÔvî¾j;[¼D¢1Š3\òëµì8ÐËO_ØÃCŸ8‘íͽDcqf¹ùÍʽüySã°ãÄïüµŠ»¯^Àe KˆÄbüc{+9®£[oÑŠðݧªøÁG+¸¢r"2tûÂüvUí¨û<±¹‰™…nžýò©lmìÁi53£ /=¾™­#¿‹HjñøÆEdÜp:<ñÄ\xá…c]ŠÈ1yfÛ¾ûÔvB‘‘ë Åf61»ØÃb¦¹ÇOƒgø·ún»……nºýaê:ú‰ ü“è°šÉrZ‡­G(ÊpÐí %q§Û-8¬f:ú‚˜ ƒ Y=þä4 —p4–\s`·˜ÈqÙ8Г8¦Ù0¨(ÉÀb2QÕ܃Ëf!‹Óc·˜ÈuÙiî¬7'͖܉œò‚tÜ+ÛšzÈtZñ…¢ô‡"8,&òÒí4Ž2Ša6Ì(HÇf1±§µÿÀÈ $Ö€´{‡¯o9Øßò‚t vðÛ'Ýnaz~:f“A}g?ÏIqÙ,¸–a× ;ÍŠ7á_N,åŒò|>û§Mf8èñ‡ û×ìàϬ¼0^„Ú¡?3‹‰ìw´=(×ecjž‹H,ÎîV/¾PtD›wZ41‹G>}ÒÛ‰ÈÑSð9Ž(xȇݻ òÏeIY³œÔwú˜–ïâ«g—óݧªxuwûX—6‚‚‡ÈûOS­DDDäÑÙâ‚9…œ=«€Ž¾ ßXñvò¡‰"2þ)xˆˆˆÈ¢¦½;ŸÝ9ÖeˆÈÑ]­DDDDD$å{ZýÁf“ÁÏìàÕ݉åÿ~É\‚‘('LÎ&ÇeÃl2øâ£›¹°¢ˆ ç‘á´òè›ûùÙ‹{¸aÉ$OÊÆe·0£ t»…;žÙÓjæ gL%Ýná­ýÝ|éñÍDcq Ýv¸ñÒíbñ8áhŒo=QÅ–Æn¾}þLl³ŠÜLÈt’n·ð½§·ó\U yévî¾zSrÒEc¬ØÜtT׿âŠ".žWÌÛ¾˜­=\0·—ÝŒ|ùñ-¼ÝÔÀ¿7“.(¦?!Çeã± ÜõR5ÓòÓùý •¬ØÜÄU•ÉpXx¶ª%q ¯œÛn!‰ñ™ÿÞÄþ®Ät¸ò‚t~xiùn;V³ÁúÚ.njû¨AKDDd¬(xˆ§æ•dÒãSÓÞ7j›Ó¦çñ™eS¸æ÷ë©ïôqÖŒ|~~å|.¼w ­½œ63§—çqÝoÒÜãççÎà¾ë+ùݪZμk%¥Ùi<ý¥SøŸ4x|Ø-&Ξ™Ï'ÚÈ[ Ý|d^1·_<›loåÜ»Wá°šyò 'söÌ^ÜÙJ(ÊÝLƒ'ñ!ûòE%üàÒ¹|äWk€Ä}öÏŸSÈõx“ÚŽ~.ª(â¶ gó÷ªâ$‚IKO€›þ¸“ÉàžkŽèãü’LnY^ž|ý؆li¶CßÃÿÊÊΙYÀ%¿ZC—/Dei÷^»ˆ‹ï]ÇÆe·pÖÌ|nøÃ›tùB|ýœr¾¾¼œO>´€¿mmæç/î!“ë²ñè§—°¾¶‹µµ˜ (t; gÞµ’B·çn^FYž‹ÿa_ˆ]VÁg—•qûSÛ±[LüêÚEüöõZVlnÂj6ñ_WÍçs§•ñ‹WjŽõ†ˆˆHJè®V"Ç)§ÕLÏ!¦5 uÞœBž~û@r±ù«{ÚÙÛÑÏåyÉ6ÏVH®‘XUÝÓjæáõûhðøØ×écZ¾+Ùþ­ýݼÕ±x½º·ÝÂÃë÷ÇéEؼ¿›éíû‚¼0UqÓÒɸíLÍsá¶~gòâÎ6j;úÄúŒì4+¹évL†ÁòÙü~uÑÑ’?¬­W .]0g«ಛ)ÍvÒÞä@O€E¥ÙÉ6ÙÔ˜œ‚õò®6¦ç§'·íiõrÂäl®^<‘Ë–Ðã1¯$3¹=NœÖÔÐê ²»ÕËË»Úèò%¦X­®éL^Ï'gc5›x³¾‹Òl'Ev^ÞÕÆéåù﹟"""ï7xˆ§Úû‚f80±ø¡×ä§ÛÙ´ß3ì½–žùéƒw‚^‚‘Þ@xØñ‚‘vËàwC×p„"ÑÇD¢Ø-‰Ñ†9Åüþ†Å¼¸³•FŸØÀz·ÃŠw`íˆgÈ‹`$F<‹ ·Ã‚Íl¢ÝLnoí ŒèãÛM=É©NG£8ÓÁEÅœ25w°O¾ñ!}î~Ç59Ø“apßu‹pÙ-¬®é A†cð¯â@8F`È:`$6â;®OQ¦ƒ4«™;>2gX§a‰ˆˆü3Qð9N­¯íÂdÀùs ùûö–aÛ†‘–ÞÀˆ§DOÌvòÚ^¿_._TÂ_·6ñÓkD&å¤ñµsʰWB¯?L £8Ó‘}x?žxÝÜà¹m†­ƒ9ZSrÓX<9›%ÿÿ•ä"ø“¦da¯ÃÔÒ ‰ò¹?½Et”ð(""òÏBS­DŽSí}A~»ªŽÛ.œÅ9³ pÛ-˜M§NËå¾ëð̶\TQÄœâ .[8’,grqyªùCQ¦å¥c5›pXÍÜr”¡ <·í_Ï˜Ö ãßÍgMWðyŸiª•ˆˆŒjÛ¶mÜzë­”””pê©§²bÅŠ±.IDD>¤4â!""ÃÔÕÕñØcñàƒR]]Ãá ‰‘‘c¡à!""tvv²bÅ î¿ÿ~6n܈Íf# $C‡ˆˆÈ{¡à!"rœ úúxøá‡yôÑGy饗°X,ɰqð¿"""ï‘ãL,ä§í…ßó‹­/ ‡’ïG£Ñ#î»bÅ ÃHeyGTP¾Üë<¦5ˆˆÈ»§à!"rœ1Ùœ^ô%οèÌ{Wñä“O %H$9ì¾gœqßûÞ÷>ˆ2Gõ³×›¨ÕR‘‘ãa23uá2~xçç ¼øâ‹<þøã¬X±‚X,F4%‹Ø///3Ï<óƒ/xˆê7€n§+"ò¡£Û銈ç—\r <ò­­­Üÿýœ{ÍfìvûX—'""ã„‚‡ˆˆ$effrã7òüóÏÓÔÔÄOúS*++1 cÌ×vˆˆÈ‡›¦Z‰ˆ‡Ž&BróÍ7sóÍ7'Ÿíqp-ˆˆˆÈ»¥à!2ÎØ,& Ü¢1­¾•Ñ@všõ¨Û—••ñï|'u‰ˆÈ¸§à!2ÎÌ.róÚ-§u""""Ãh‡ˆˆˆˆˆ¤œ‚‡ˆˆˆˆˆ¤œ‚‡ˆˆˆˆˆ¤œ‚‡ˆˆŒKYN+7-Ì/ËmÎâÊÊœVóX—•…þñ•Ó’¯Ïœ‘ϲi¹ïù¸“sÒxê‹§óþ 'fñÐ'N<æý¿qî >¾dn‡…ÏœZ6â—ËfáOŸ<‰y%™\2¿˜|tî1ŸSDRGÁCDDÆŠ –MÏãÑOŸ4Ö¥¥„ÅdPšíL¾¾`n!Ëg¾çãZÍ&J²œGn8 »ÕDQ†ã˜÷ÏN³‘áLÜ}-Ëiã–ååe:ÈpZ’¿ VV·ãéOÜê9Ýn!/]¾ùg¤»Z‰ˆÈ¸b1ü×U x®ª…<·3ùþŸ75R1!#ùÚæ•dR”á º­ºÎþä¶Òl'þp ›ÙÄ‚‰™Ôuö³«Å‹ÕlbIYfÃ`}]'ÁH €¼t;N«‰Þ@„¥e9x|a6ÔwP9)›ì4+oÖ{è „“çÈO·S1!‹ÙÄν4vû“Û&d:‰ÆbDãpÂälZ{lnèÖÏ—ÊÒ,ºýaôF½&Ã`v‘›Ý­^æOÌ$×egkc7mÞà°v…æg'ÎÖ†º|#ŸÙ2-?6oo €ÓjfRN»[½Ãj¯(É Éã±?@†Ãʼ’ Ì&ƒ- =î‰,˜˜Ea† û<‡Üÿ·¯×ÒÞ7¼ö•{ÚYïA¹.óJ2 EblnèÆŽŽÚVDRGÁCDDÆ•¥Ss)Îtpï«5#¶U5÷‰oòõ/ ™å¤ª¹‡Û/žÍ_65òË}¾vN9YN+N+õ>Ξ™ÏžßÅÅÅtùBLËO§Û7™O>´‘8pÙ œ3«‹É ¶£Ÿ%e9<óö²Òld¥YqÙÌ|çÂY\úëµxƒ¦ä¦ñ»³e LÜyÉ~úžÜÒÀçO/£4;L§•½í}œ<5—'·4q×KÕÌ/Éä¾ë+y³¾ ›Ù„Å4ú#!mùÜR^ÜÙŠÝb&Nœ\:—kïƒÚŽDØúÄÉ“ùüiSY[ÛI<ÿzÚT®¹ÿÇúÙó¸çÕ½¼²» €ééüæÚE,ûÙkœ3«€ÿüX+÷´S”á ðŽøKÊrøñeó†ô{._zl3;$~.?¾|óK2Ù´ßÃΘF ¥¹çÐf¨ß\WÉ·žÜÆÆC„•‹*Џõ‚Y¬«íÄí°ð½‹góɇ76¬‰Hj(xˆˆÈ¸2=ßE›7H·?¸Mû=8­f^ÿÆÌ*r³s ýäœ4.ºw5_˜…³xäS'ñ£ìâOoìàÏŸ]ÊòÙ…<¹¥‰Ÿ ~¹šX<ñ Ïy%™Üw]e2xfعü¾u#1*&dò§OÈ/^©!‹së³xpm=¿[]À·ÎŸÉ²éy‡½.›ºypÝ>~|Ù<®¬œÈO^ØÍÌB7_9»œk~¿žê¶>ÌÆÑ<Û~8‹Éà»ÍæûOïàùí-À/¯Y˜Ün·˜øÑÇ*øþÓ;XYÝÀ5'”òoçÍàmdÙ´\N™šË…÷¬ÆŒPà¶ó÷›—±ª¦cØyîºj>¡‘¦mͽÜýrõ¨5åºl|ÿâ9Üøà†ä¨Ì-ËËùò™Ó¹íoUïº"òÞ(xˆˆÈ¸b‘Xü°mOÊæ…­>Àîïòñvc‹'e'ƒÇëÕÉãÔ´õÑŒ°iâu8J£ÇÏ„,g2xlnèÆãK„šö> ^ÛÝžøÏbpȾÑX|ĺ€h,Žyȧ¡Ûÿ;Žqp$áü9…|ó¼ÜõR5 ?V“Áy³ ±[Ì@âCõÐú¢Ç3›L8mfL†1lûѬY^_,Y‹Ûn¡oàÃþ»5t†—Ën&‰ -CÏévXGcd8¬ƒuDãüze-&“Ëfñ39T¿6íóŒXã1š §uÄ9;úB<¶¡á¨ö‘÷—‚‡ˆˆŒ+kövâ F¸qɤäT¤ƒrÒltùBÔwú˜S<¸ÐÜl2˜YèæÉ-ÍH§MÏão[ð\U •¥YG½o?ŒÇfV‘›µµÌ.rs-Õí}\¾¨«ÙD8;lÛn˜ì´ÁñSóÒ“¿ßßåÇa5SšFƒÇÀ¬!uÕuôc³˜x~{˰…ôíëòqaEQòµÌ,tSÓÞ7¢íÑÚÛÞGºÝÂÿllHޤˆÈØÑítEDd\ñ‡£Üö·*þõô©|ûü™œ53Ÿ%e9|å¬éüî†J}³eÓóøÄÉ“©˜Á™C ãÕ!S£R©¶£Ÿå³ X01““§ær˹3ÞÕþ¯ßÇ­Ìä„ÉÙœ:-—OŸ:å˜kyyWÝþ0?¹|‹J³XTšÅ ÏÎx§7뺸qédNš’Ãùs ¹éäÁé]½0Ommæ—Îe^I&çÏ)äŠE%ÉímÞ ½ÙÀ/¯YÈéåyÌ,tsÞœBþõô©<õv3e¹.¾xÆ4f¹ùæy3ÉM·s¿¶6ö°¡ÞÃ/ÿe!KÊr˜]äæ#óйþ¤C÷ODRË|ÇwÜ1ÖEˆˆˆ­'·4ñŽDõ>^ÙÝÎÂÒ,ÎUÈü’L<¾0?ya7}Á½0+«;8ov!çÏ-¢¥'ÀwþZE(ñ­x¦ÃJ]§ý]‰oî-fV³‰u# xÆDUs/í}AÒlz|áäÝ™ à ×ecUM¡Q„ ‡•FŸºÎ~¶7÷’—nãªÅ¥LÉIãî—«1 ƒ×«;FSƒ»ýÔ ÜuÊòÝvVÕtˆÄxk¿›ÅÄÕ‹'2%×Å=¯î%‹óÚžDprœkO[ß!kqÚÌxªš{‰ÇkD¦å§sé‚ œ0%›†.?[{°˜ ÒlæäïmM=ä¹í\¶°„L§•{_ÛK$gíÞÄuYUÓÉÄl'WVN$;Íʯ^ÛK0c}]âÖ«j¸¨¢ˆ *ŠÈO·³¾®‹úN¡H"ø;»€‹*Ѝjîáå]m4tù©ïôa1¤;,¬ªìÇA¹.›öyèñ‡qZÍô‡¢lkêà…­¸V.žW̹³ q;,¬ÞÛIóþ Ý´t2n‡&†ˆ¼ŸŒxüV‰ˆˆŒ‘?¸M£<ãAäýòò×N§8óØ~("#iª•ˆˆˆˆˆ¤œ‚‡ˆˆˆˆˆ¤œ‚‡ˆˆˆˆˆ¤œ‚‡ˆˆˆˆˆ¤œ‚‡ˆˆˆˆˆ¤œ‚‡ˆˆˆˆˆ¤œ‚‡ˆˆˆˆˆ¤œ‚‡ˆˆˆˆˆ¤œ‚‡ˆˆˆˆˆ¤œ‚‡ˆˆˆˆˆ¤œ‚‡ˆˆˆˆˆ¤œ‚‡ˆˆˆˆˆ¤œ‚‡ˆˆˆˆˆ¤œ‚‡ˆˆˆˆˆ¤œ‚‡ˆˆˆˆˆ¤œ‚‡ˆˆˆˆˆ¤œ‚‡ˆˆˆˆˆ¤œ‚‡ˆˆˆˆˆ¤œe¬ y7Ξ‘Oi–s¬Ëq.ÍfëDÆ#ÇǺß4ÕJDDDDDRNÁCDDDDDRNÁCDDDDDRîÿýô~x¿õà.IEND®B`‚pibootctl-0.5.2/docs/images/setting_hierarchy.svg000066400000000000000000000231411372751746400221670ustar00rootroot00000000000000 classes Setting Setting Overlay Overlay Overlay->Setting OverlayParam OverlayParam OverlayParam->Overlay OverlayParamInt OverlayParamInt OverlayParamInt->OverlayParam OverlayParamBool OverlayParamBool OverlayParamBool->OverlayParam Command Command Command->Setting CommandInt CommandInt CommandInt->Command CommandIntHex CommandIntHex CommandIntHex->CommandInt CommandBool CommandBool CommandBool->Command CommandBoolInv CommandBoolInv CommandBoolInv->CommandBool CommandForceIgnore CommandForceIgnore CommandForceIgnore->CommandBool CommandMaskMaster CommandMaskMaster CommandMaskMaster->CommandInt CommandMaskDummy CommandMaskDummy CommandMaskDummy->CommandMaskMaster CommandFilename CommandFilename CommandFilename->Command CommandIncludedFile CommandIncludedFile CommandIncludedFile->CommandFilename pibootctl-0.5.2/docs/index.rst000066400000000000000000000017451372751746400163350ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . .. include:: ../README.rst Contents ======== .. toctree:: :maxdepth: 2 :includehidden: install manual development api changelog license Indexes ======= * :ref:`genindex` * :ref:`search` pibootctl-0.5.2/docs/install.rst000066400000000000000000000102121372751746400166610ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . ============ Installation ============ If your distribution provides pibootctl then you should either find the utility is installed by default, or it should be installable via your package manager. For example: .. code-block:: console $ sudo apt install pibootctl It is strongly recommended to use a provided package rather than installing from PyPI as this will include configuration specific to your distribution. The utility can be removed via the usual mechanism for your package manager. For instance: .. code-block:: console $ sudo apt purge pibootctl Configuration ============= pibootctl looks for its configuration in three locations: #. :file:`/lib/pibootctl/pibootctl.conf` #. :file:`/etc/pibootctl.conf` #. :file:`~/.config/pibootctl.conf` The last location is only intended for use by people developing pibootctl; for the vast majority of users the configuration should be provided by their distribution in one of the first two locations. The configuration file is a straight-forward INI-style containing a single section titled "defaults". A typical configuration file might look like this: .. code-block:: ini :caption: pibootctl.conf [defaults] boot_path = /boot store_path = pibootctl package_name = pibootctl comment_lines = on backup = on The configuration specifies several settings, but the most important are: ``boot_path`` The mount-point of the boot partition (defaults to :file:`/boot`). ``store_path`` The path under which to store saved boot configurations, relative to ``boot_path`` (defaults to :file:`pibootctl`). ``config_root`` The "root" configuration file which is read first, relative to ``boot_path`` (defaults to :file:`config.txt`). This is also the primary file that gets re-written when settings are changed. ``mutable_files`` The set of files within a configuration that may be modified by the utility (defaults to :file:`config.txt`). List multiple files on separate lines. Currently, this *must* include :file:`config.txt`. ``comment_lines`` If this is on, when lines in configuration files are no longer required, they will be commented out with a "#" prefix instead of being deleted. Defaults to off. Note that, regardless of this setting, the utility will always search for commented lines to uncomment before writing new ones. ``reboot_required`` The file which should be created in the event that the active boot configuration is changed. ``reboot_required_pkgs`` The file to which the value of ``package_name`` should be appended in the event that the active boot configuration is changed. ``package_name`` The name of the package which contains the utility. Used by ``reboot_required_pkgs``. ``backup`` If this is on (the default), any attempt to change the active boot configuration will automatically create a backup of that configuration if one does not already exist. Line comments can be included in the configuration file with a ``#`` prefix. Another example configuration, typical for Ubuntu on the Raspberry Pi, is shown below: .. code-block:: ini :caption: pibootctl.conf [defaults] boot_path = /boot store_path = pibootctl mutable_files = config.txt syscfg.txt reboot_required = /var/run/reboot-required reboot_required_pkgs = /var/run/reboot-required.pkgs package_name = pibootctl backup = on pibootctl-0.5.2/docs/license.rst000066400000000000000000000012261372751746400166420ustar00rootroot00000000000000======= License ======= This file is part of pibootctl. pibootctl is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. pibootctl is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with pibootctl. If not, see . pibootctl-0.5.2/docs/list.rst000066400000000000000000000057051372751746400162010ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . ==== list ==== .. program:: pibootctl-list Synopsis ======== .. code-block:: text pibootctl list [-h] [--json | --yaml | --shell] Description =========== List all stored boot configurations. Options ======= .. option:: -h, --help Show a brief help page for the command. .. option:: --json Use JSON as the output format. .. option:: --yaml Use YAML as the output format. .. option:: --shell Use a tab-delimited output format suitable for the shell. Usage ===== The :command:`list` command is used to display the content of the store of boot configurations: .. code-block:: console $ pibootctl list +---------+--------+---------------------+ | Name | Active | Timestamp | |---------+--------+---------------------| | 720p | x | 2020-03-10 11:33:24 | | default | | 2020-03-10 11:32:12 | | dpi | | 2020-02-01 15:46:48 | | gpi | | 2020-02-01 16:13:02 | +---------+--------+---------------------+ If one (or more) of the stored configurations match the current boot configuration, this will be indicated in the "Active" column. Note that equivalence is based on a hash of all files in the configuration, not on the resulting settings. Hence a simple edit like, for example, reversing the order of two lines (which might not make any difference to the resulting settings) would be sufficient to mark the configuration as "different". The "timestamp" of a stored configuration is the last modification date of that configuration (calculated as the latest modification date of all files within the configuration). For developers wishing to build on top of pibootctl, options are provided to produce the output in JSON (:option:`--json`), YAML (:option:`--yaml`), and shell-friendly (:option:`--shell`). These combine with all aforementioned options as expected: .. code-block:: console $ pibootctl list --json [{"timestamp": "2020-02-01T15:46:48", "active": false, "name": "dpi"}, {"timestamp": "2020-03-10T11:32:12", "active": false, "name": "default"}, {"timestamp": "2020-02-01T16:13:02", "active": false, "name": "gpi"}, {"timestamp": "2020-03-10T11:33:24", "active": true, "name": "720p"}] pibootctl-0.5.2/docs/load.rst000066400000000000000000000047201372751746400161410ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . ==== load ==== .. program:: pibootctl-load Synopsis ======== .. code-block:: text pibootctl load [-h] [--no-backup] name Description =========== Overwrite the current boot configuration with a stored one. Options ======= .. option:: name The name of the boot configuration to restore .. option:: -h, --help Show a brief help page for the command. .. option:: --no-backup Don't take an automatic backup of the current boot configuration if one doesn't exist Usage ===== The :command:`load` command is used to replace the current boot configuration with one previously stored. Effectively this simply unpacks the `PKZIP`_ of the stored boot configuration into the boot partition, overwriting existing files. If the current boot configuration has not been stored (with the :doc:`save` command), an automatically named backup will be saved first: .. code-block:: console $ sudo pibootctl save default $ sudo pibootctl set video.hdmi0.group=1 video.hdmi0.mode=4 $ sudo pibootctl load default Backed up current configuration in backup-20200310-095646 This can be avoided with the :option:`--no-backup` option. .. warning:: The command is written to guarantee that no files will ever be left half-written (files are unpacked to a temporary filename then atomically moved into their final location overwriting any existing file). However, the utility cannot guarantee that in the event of an error, the configuration as a whole is not half-written (i.e. that one or more files failed to unpack). In other words, in the event of failure you cannot assume that the boot configuration is consistent. .. _PKZIP: https://en.wikipedia.org/wiki/Zip_(file_format) pibootctl-0.5.2/docs/manual.rst000066400000000000000000000022011372751746400164670ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . =========== User Manual =========== The :command:`pibootctl` utility defines several commands which can be used to query and manipulate the boot configuration of the Raspberry Pi: .. include:: commands.rst .. include:: usage.rst .. toctree:: :maxdepth: 1 :hidden: diff get help list load remove rename save set show status pibootctl-0.5.2/docs/pibootctl.rst000066400000000000000000000024011372751746400172130ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . ========= pibootctl ========= .. program:: pibootctl Synopsis ======== .. code-block:: text pibootctl [-h] [--version] command ... Description =========== The :command:`pibootctl` utility exists to query and manipulate the boot configuration of the Raspberry Pi. It also permits easy storage and retrieval of boot configurations. Each of the commands provided by the utility are listed in the following section. Commands ======== .. include:: commands.rst Usage ===== .. include:: usage.rst pibootctl-0.5.2/docs/remove.rst000066400000000000000000000044771372751746400165300ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . ====== remove ====== .. program:: pibootctl-remove Synopsis ======== .. code-block:: text pibootctl remove [-h] [-f] name Description =========== Remove a stored boot configuration. Options ======= .. option:: name The name of the boot configuration to remove. .. option:: -h, --help Show a brief help page for the command. .. option:: -f, --force Ignore errors if the named configuration does not exist. Usage ===== The :command:`remove` command is used to delete a stored boot configuration: .. code-block:: console $ pibootctl list +---------+--------+---------------------+ | Name | Active | Timestamp | |---------+--------+---------------------| | 720p | x | 2020-03-10 11:33:24 | | default | | 2020-03-10 11:32:12 | | dpi | | 2020-02-01 15:46:48 | | gpi | | 2020-02-01 16:13:02 | +---------+--------+---------------------+ $ sudo pibootctl remove gpi $ pibootctl list +---------+--------+---------------------+ | Name | Active | Timestamp | |---------+--------+---------------------| | 720p | x | 2020-03-10 11:33:24 | | default | | 2020-03-10 11:32:12 | | dpi | | 2020-02-01 15:46:48 | +---------+--------+---------------------+ If, for scripting purposes, you wish to ignore the error in the case the specified stored configuration does not exist, use the :option:`--force` option: .. code-block:: console $ pibootctl rm foo unknown configuration foo $ pibootctl rm -f foo pibootctl-0.5.2/docs/rename.rst000066400000000000000000000056221372751746400164730ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . ====== rename ====== .. program:: pibootctl-rename Synopsis ======== .. code-block:: text pibootctl rename [-h] [-f] name to Description =========== Rename a stored boot configuration. Options ======= .. option:: name The name of the boot configuration to rename. .. option:: to The new name of the boot configuration. .. option:: -h, --help Show a brief help page for the command. .. option:: -f, --force Overwrite the target configuration, if it exists. Usage ===== The :command:`rename` command can be used to change the name of a stored boot configuration: .. code-block:: console $ pibootctl ls +---------+--------+---------------------+ | Name | Active | Timestamp | |---------+--------+---------------------| | 720p | x | 2020-03-10 11:33:24 | | default | | 2020-03-10 11:32:12 | | dpi | | 2020-02-01 15:46:48 | +---------+--------+---------------------+ $ sudo pibootctl rename default foo $ pibootctl ls +------+--------+---------------------+ | Name | Active | Timestamp | |------+--------+---------------------| | 720p | x | 2020-03-10 11:33:24 | | dpi | | 2020-02-01 15:46:48 | | foo | | 2020-03-10 11:32:12 | +------+--------+---------------------+ As with :doc:`save`, any characters permitted in a filename are permitted in the new destination name. If you wish to rename a configuration such that it overwrites an existing configuration you will need to use the :option:`--force` option: .. code-block:: console $ sudo pibootctl load default $ sudo pibootctl save foo $ pibootctl ls +---------+--------+---------------------+ | Name | Active | Timestamp | |---------+--------+---------------------| | 720p | | 2020-03-10 11:33:24 | | default | x | 2020-03-10 11:32:12 | | dpi | | 2020-02-01 15:46:48 | | foo | x | 2020-03-10 11:32:12 | +---------+--------+---------------------+ $ sudo pibootctl mv foo default [Errno 17] File exists: 'default.zip' $ sudo pibootctl mv -f foo default pibootctl-0.5.2/docs/save.rst000066400000000000000000000045251372751746400161630ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . ==== save ==== .. program:: pibootctl-save Synopsis ======== .. code-block:: text pibootctl save [-h] [-f] name Description =========== Store the current boot configuration under a given name. Options ======= .. option:: name The name to save the current boot configuration under; can include any characters legal in a filename .. option:: -h, --help Show a brief help page for the command. .. option:: -f, --force Overwrite an existing configuration, if one exists Usage ===== The :command:`save` command is used to take a backup of the current boot configuration. In practice this creates a `PKZIP`_ of the files that make up the boot configuration (:file:`config.txt` et al.), and places it under the configured directory on the boot partition (usually :file:`pibootctl`): .. code-block:: console $ ls /boot/pibootctl $ sudo pibootctl save foo $ ls /boot/pibootctl foo.zip Note that by default, you cannot overwrite saved configurations, but this can be overridden with the :option:`--force` option: .. code-block:: console $ sudo pibootctl save foo [Errno 17] File exists: 'foo.zip' $ sudo pibootctl save -f foo In the event that your system is rendered un-bootable, a boot configuration can be easily restored by extracting the PKZIP of a saved configuration into the boot partition (over-writing files as necessary). Alternatively you can use the :doc:`load` command (if the system can boot). The :doc:`list` command can be used to display all currently stored configurations. .. _PKZIP: https://en.wikipedia.org/wiki/Zip_(file_format) pibootctl-0.5.2/docs/set.rst000066400000000000000000000065451372751746400160240ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . === set === .. program:: pibootctl-set Synopsis ======== .. code-block:: text pibootctl set [-h] [--no-backup] [--all | --this-model | --this-serial] [--json] [--yaml] [--shell] [name=[value] [name=[value] ...]] Description =========== Change the value of one or more boot configuration settings. To reset the value of a setting to its default, simply omit the new value. Options ======= .. option:: name=[value] Specify one or more settings to change on the command line; to reset a setting to its default omit the value. .. option:: -h, --help Show a brief help page for the command. .. option:: --no-backup Don't take an automatic backup of the current boot configuration if one doesn't exist. .. option:: --all Set the specified settings on all Pis this SD card is used with. This is the default context. .. option:: --this-model Set the specified settings for this model of Pi only. .. option:: --this-serial Set the specified settings for this Pi's serial number only. .. option:: --json Use JSON as the input format. .. option:: --yaml Use YAML as the input format. .. option:: --shell Use a var=value input format suitable for the shell. Usage ===== The :command:`set` command can be used at the command line to update the boot configuration: .. code-block:: console $ sudo pibootctl set video.overscan.enabled=off Backed up current configuration in backup-20200309-230959 Note that, if no backup of the current boot configuration exists, a backup is automatically taken (unless :option:`--no-backup` is specified). Multiple settings can be changed at once, and settings can be reset to their default value by omitting the new value after the "=" sign: .. code-block:: console $ sudo pibootctl set --no-backup serial.enabled=on serial.uart= By default, settings are written into an "[all]" section in :file:`config.txt` meaning that they will apply everywhere the SD card is moved. However, you can opt to make settings specific to the current model of Pi, or even the current Pi's serial number: .. code-block:: console $ sudo pibootctl set --this-serial camera.enabled=on gpu.mem=128 In this case an appropriate section like "[0x123456789]" will be added and the settings written under there. For those wishing to build an interface on top of pibootctl, JSON, YAML, and shell-friendly formats can also be used to feed new values to the :command:`set` command: .. code-block:: console $ cat << EOF | sudo pibootctl set --json --no-backup {"serial.enabled": true, "serial.uart": null} EOF pibootctl-0.5.2/docs/show.rst000066400000000000000000000113551372751746400162040ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . ==== show ==== .. program:: pibootctl-show Synopsis ======== .. code-block:: text pibootctl show [-h] [-a] [--json | --yaml | --shell] name [pattern] Description =========== Display the specified stored boot configuration, or the sub-set of its settings that match the specified pattern. Options ======= .. option:: name The name of the boot configuration to display. .. option:: pattern If specified, only displays settings with names that match the specified pattern which may include shell globbing characters (e.g. \*, ?, and simple [classes]) .. option:: -h, --help Show a brief help page for the command. .. option:: -a, --all Include all settings, regardless of modification, in the output; by default, only settings which have been modified are included. .. option:: --json Use JSON as the output format. .. option:: --yaml Use YAML as the output format. .. option:: --shell Use a var=value output format suitable for the shell. Usage ===== The :command:`show` command is the equivalent of the :doc:`status` command for stored boot configurations. By default it displays only the settings in the specified configuration that have been modified from their default: .. code-block:: console $ pibootctl show 720p +------------------------+----------------+ | Name | Value | |------------------------+----------------| | video.hdmi0.group | 1 (CEA) | | video.hdmi0.mode | 4 (720p @60Hz) | +------------------------+----------------+ The full set of settings can be displayed (which is usually several pages long, and thus will implicitly invoke the system's pager) can be displayed with the :option:`--all` option: .. code-block:: console $ pibootctl show 720p --all +------------------------------+----------+--------------------------------+ | Name | Modified | Value | |------------------------------+----------+--------------------------------| ... | video.hdmi0.enabled | | auto | | video.hdmi0.encoding | | 0 (auto; 1 for CEA, 2 for DMT) | | video.hdmi0.flip | | 0 (none) | | video.hdmi0.group | x | 1 (CEA) | | video.hdmi0.mode | x | 4 (720p @60Hz) | | video.hdmi0.mode.force | | off | | video.hdmi0.rotate | | 0 | | video.hdmi0.timings | | [] | | video.hdmi1.audio | | auto | | video.hdmi1.boost | | 5 | ... Note that when :option:`--all` is specified, a "Modified" column is included in the output to indicate which settings are no longer default. As with the :doc:`status` command, the list of settings can be further filtered by specified a *pattern* with the command. The *pattern* can include any of the common shell wildcard characters: * ``*`` for any number of any character * ``?`` for any single character * ``[seq]`` for any character in *seq* * ``[!seq]`` for any character not in *seq* For example: .. code-block:: console $ pibootctl show --all 720p i2c.* +-------------+----------+--------+ | Name | Modified | Value | |-------------+----------+--------| | i2c.baud | | 100000 | | i2c.enabled | | off | +-------------+----------+--------+ For developers wishing to build on top of pibootctl, options are provided to produce the output in JSON (:option:`--json`), YAML (:option:`--yaml`), and shell-friendly (:option:`--shell`). These combine with all aforementioned options as expected: .. code-block:: console $ pibootctl show --json --all 720p i2c.* {"i2c.baud": 100000, "i2c.enabled": false} pibootctl-0.5.2/docs/status.rst000066400000000000000000000105201372751746400165400ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . ====== status ====== .. program:: pibootctl-status Synopsis ======== .. code-block:: text pibootctl status [-h] [-a] [--json | --yaml | --shell] [pattern] Description =========== Output the current value of modified boot time settings that match the specified pattern (or all if no pattern is provided). The :option:`--all` option may be specified to output all boot settings regardless of modification state. Options ======= .. option:: pattern If specified, only displays settings with names that match the specified *pattern* which may include shell globbing characters (e.g. \*, ?, and simple [classes]). .. option:: -h, --help Show a brief help page for the command. .. option:: -a, --all Include all settings, regardless of modification, in the output. By default, only settings which have been modified are included. .. option:: --json Use JSON as the output format. .. option:: --yaml Use YAML as the output format. .. option:: --shell Use a var=value format suitable for the shell. Usage ===== By default, the :command:`status` command only outputs boot time settings which have been modified: .. code-block:: console $ pibootctl status +-------------+-------+ | Name | Value | |-------------+-------| | i2c.enabled | on | | spi.enabled | on | +-------------+-------+ The full set of settings (which is usually several pages long, and thus will implicitly invoke the system's pager) can be displayed with the :option:`--all` option: .. code-block:: console $ pibootctl status --all +------------------------------+----------+--------------------------+ | Name | Modified | Value | |------------------------------+----------+--------------------------| ... | i2c.baud | | 100000 | | i2c.enabled | x | on | | i2s.enabled | | off | | serial.baud | | 115200 | | serial.clock | | 48000000 | | serial.enabled | | on | | serial.uart | | 0 (/dev/ttyAMA0; PL011) | | spi.enabled | x | on | | video.cec.enabled | | on | ... Note that when :option:`--all` is specified, a "Modified" column is included in the output to indicate which settings are no longer default. The list of settings can be further filtered by specified a *pattern* with the command. The *pattern* can include any of the common shell wildcard characters: * ``*`` for any number of any character * ``?`` for any single character * ``[seq]`` for any character in *seq* * ``[!seq]`` for any character not in *seq* For example: .. code-block:: console $ pibootctl status --all i2c.* +-------------+----------+--------+ | Name | Modified | Value | |-------------+----------+--------| | i2c.baud | | 100000 | | i2c.enabled | x | on | +-------------+----------+--------+ For developers wishing to build on top of pibootctl, options are provided to produce the output in JSON (:option:`--json`), YAML (:option:`--yaml`), and shell-friendly (:option:`--shell`). These combine with all aforementioned options as expected: .. code-block:: console $ pibootctl status --json --all i2c.* {"i2c.baud": 100000, "i2c.enabled": true} pibootctl-0.5.2/docs/usage.rst000066400000000000000000000134061372751746400163270ustar00rootroot00000000000000.. Copyright (c) 2020 Canonical Ltd. .. Copyright (c) 2020 Dave Jones .. .. This file is part of pibootctl. .. .. pibootctl is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. pibootctl is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with pibootctl. If not, see . Typically, the :doc:`status` command is the first used, to determine the current boot configuration: .. code-block:: console $ pibootctl status +------------------------+-------+ | Name | Value | |------------------------+-------| | i2c.enabled | on | | spi.enabled | on | | video.overscan.enabled | off | +------------------------+-------+ After which the :doc:`save` command might be used to take a backup of the configuration before editing it with the :doc:`set` command: .. code-block:: console $ sudo pibootctl save default $ sudo pibootctl set camera.enabled=on gpu.mem=128 $ sudo pibootctl save cam .. note:: Note that commands which modify the content of the boot partition (e.g. :doc:`save` and :doc:`set`) are executed with :command:`sudo` as root privileges are typically required. The configuration of :program:`pibootctl` itself dictates where the stored configurations are placed on disk. By default this is under a "pibootctl" directory on the boot partition, but this can be changed in the :program:`pibootctl` configuration. The application attempts to read its configuration from the following locations on startup: * :file:`/lib/pibootctl/pibootctl.conf` * :file:`/etc/pibootctl.conf` * :file:`$XDG_CONFIG_HOME/pibootctl.conf` The final location is only intended for developers working on :program:`pibootctl` itself. The others should be used by packages providing pibootctl on your chosen OS. Stored boot configurations are simply `PKZIP`_ files containing the files that make up the boot configuration (sometimes this is just the :file:`config.txt` file, and sometimes other files may be included). .. note:: In the event that your system is unable to boot (e.g. because of mis-configuration), you can restore a stored boot configuration simply by unzipping the stored configuration back into the root of the boot partition. In other words, you can simply place your Pi's SD card in a Windows or MAC OS X computer which should automatically mount the boot partition (which is the only partition that these OS' will understand on the card), find the "pibootctl" folder and under there you should see all your stored configurations as .zip files. Unzip one of these into the folder above "pibootctl", overwriting files as necessary and you have restored your boot configuration. The :doc:`diff` command can be used to discover the differences between boot configurations: .. code-block:: console $ pibootctl diff default +------------------------+---------------+-------------+ | Name | | default | |------------------------+---------------+-------------| | boot.firmware.filename | 'start_x.elf' | 'start.elf' | | boot.firmware.fixup | 'fixup_x.dat' | 'fixup.dat' | | camera.enabled | on | off | | gpu.mem | 128 (Mb) | 64 (Mb) | +------------------------+---------------+-------------+ .. note:: Some settings indirectly affect others. Even though we did not explicitly set ``boot.firmware.filename``, setting ``camera.enabled`` affected its default value. The :doc:`help` command can be used to display the help screen for each sub-command: .. code-block:: console $ pibootctl help save usage: pibootctl save [-h] [-f] name Store the current boot configuration under a given name. positional arguments: name The name to save the current boot configuration under; can include any characters legal in a filename optional arguments: -h, --help show this help message and exit -f, --force Overwrite an existing configuration, if one exists Additionally, :doc:`help` will accept setting names to display information about the defaults and underlying commands each setting represents: .. code-block:: console $ pibootctl help camera.enabled Name: camera.enabled Default: off Command(s): start_x, start_debug, start_file, fixup_file Enables loading the Pi camera module firmware. This implies that start_x.elf (or start4x.elf) will be loaded as the GPU firmware rather than the default start.elf (and the corresponding fixup file). Note: with the camera firmware loaded, gpu.mem must be 64Mb or larger (128Mb is recommended for most purposes; 256Mb may be required for complex processing pipelines). The :doc:`list` command can be used to display the content of the configuration store, and :doc:`load` to restore previously saved configurations: .. code-block:: console $ pibootctl list +---------+--------+---------------------+ | Name | Active | Timestamp | |---------+--------+---------------------| | cam | x | 2020-03-11 21:29:56 | | default | | 2020-03-11 21:29:13 | +---------+--------+---------------------+ $ sudo pibootctl load default .. _PKZIP: https://en.wikipedia.org/wiki/Zip_(file_format) pibootctl-0.5.2/pibootctl/000077500000000000000000000000001372751746400155345ustar00rootroot00000000000000pibootctl-0.5.2/pibootctl/__init__.py000066400000000000000000000014121372751746400176430ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . __version__ = '0.5.2' pibootctl-0.5.2/pibootctl/exc.py000066400000000000000000000045511372751746400166720ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . """ The :mod:`pibootctl.exc` module defines the various exceptions used in the application: .. autoexception:: InvalidConfiguration .. autoexception:: IneffectiveConfiguration """ import gettext _ = gettext.gettext class InvalidConfiguration(ValueError): """ Error raised when an updated configuration fails to validate. All :exc:`ValueError` exceptions raised during validation are available from the :attr:`errors` attribute which maps setting names to the :exc:`ValueError` raised. """ def __init__(self, errors): self.errors = errors super().__init__(str(self)) def __str__(self): return _( "Configuration failed to validate with {count} error(s)").format( count=len(self.errors)) class IneffectiveConfiguration(ValueError): """ Error raised when an updated configuration has been overridden by something in a file we're not allowed to edit. All settings which have been overridden are available from the :attr:`diff` attribute. """ def __init__(self, diff): self.diff = diff super().__init__(str(self)) def __str__(self): return _("Failed to set {count} setting(s)").format( count=len(self.diff)) class DelegatedOutput(Exception): """ Exception raised when output is requested from a setting, but that setting's output is actually handled by another setting. """ def __init__(self, master): self.master = master # Not intended to be a user-seen message, hence no translation super().__init__("Output handled by {master}".format(master=master)) pibootctl-0.5.2/pibootctl/files.py000066400000000000000000000104401372751746400172070ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . """ The :mod:`pibootctl.files` module contains the :class:`AtomicReplaceFile` context manager, used to "safely" replace files by writing to a temporary file in the same directory, then moving the result over the target if no exception occurs within the block. The result is that external processes either see the "old" state of the file, or the "new" state, but nothing in between:: >>> from pathlib import Path >>> from pibootctl.files import AtomicReplaceFile >>> foo = Path('foo.txt') >>> foo.write_text('foo') >>> foo.read_text() 'foo' >>> with AtomicReplaceFile(foo, encoding='ascii') as f: ... f.write('bar') ... raise Exception('something went wrong!') ... 3 Traceback (most recent call last): File "", line 3, in Exception: something went wrong! >>> foo.read_text() 'foo' .. autoclass:: AtomicReplaceFile """ import os import tempfile import threading from pathlib import Path def get_umask(): """ Return the umask of the current process. .. warning:: This function is *not* safe in a multi-threaded context. For a brief moment, the umask of the process will be modified (as this is the only means of querying the umask without writing stuff to disk, which is subject to all sorts of caveats over location). To this end, the function will refuse to run in anything but the main thread. """ if threading.current_thread() is not threading.main_thread(): raise RuntimeError('get_umask called from thread other than main') mask = os.umask(0) os.umask(mask) return mask class AtomicReplaceFile: """ A context manager for atomically replacing a target file. Uses :func:`tempfile.NamedTemporaryFile` to construct a temporary file in the same directory as the target file. The associated file-like object is returned as the context manager's variable; you should write the content you wish to this object. When the context manager exits, if no exception has occurred, the temporary file will be renamed over the target file atomically (and sensible permissions will be set, i.e. 0666 & umask). If an exception occurs during the context manager's block, the temporary file will be deleted leaving the original target file unaffected and the exception will be re-raised. :type path: str or pathlib.Path :param path: The full path and filename of the target file. This is expected to be an absolute path. :param str encoding: If :data:`None` (the default), the temporary file will be opened in binary mode. Otherwise, this specifies the encoding to use with text mode. """ umask = get_umask() def __init__(self, path, encoding=None): if not isinstance(path, Path): path = Path(path) self._path = path self._tempfile = tempfile.NamedTemporaryFile( mode='wb' if encoding is None else 'w', dir=str(self._path.parent), encoding=encoding, delete=False) self._withfile = None def __enter__(self): self._withfile = self._tempfile.__enter__() return self._withfile def __exit__(self, exc_type, exc_value, exc_tb): os.fchmod(self._withfile.file.fileno(), 0o666 & ~AtomicReplaceFile.umask) result = self._tempfile.__exit__(exc_type, exc_value, exc_tb) if exc_type is None: os.rename(self._withfile.name, str(self._path)) else: os.unlink(self._withfile.name) return result pibootctl-0.5.2/pibootctl/formatter.py000066400000000000000000000703651372751746400201240ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2019, 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . """ The :mod:`pibootctl.formatter` module contains some generic text formatting routines, including the :class:`TableWrapper` class (akin to :class:`~textwrap.TextWrapper` but specific to table output), :class:`TransMap` for partially formatting templates, and the :func:`render` function: a crude markup renderer. .. autoclass:: TableWrapper .. data:: pretty_table Uses simple ASCII characters to produce a typical "box-like" table appearance:: >>> from pibootctl.formatter import * >>> wrapper = TableWrapper(width=80, **pretty_table) >>> data = [ ... ('Name', 'Length', 'Position'), ... ('foo', 3, 1), ... ('bar', 3, 2), ... ('baz', 3, 3), ... ('quux', 4, 4)] >>> print(wrapper.fill(data)) +------+--------+----------+ | Name | Length | Position | |------+--------+----------| | foo | 3 | 1 | | bar | 3 | 2 | | baz | 3 | 3 | | quux | 4 | 4 | +------+--------+----------+ .. data:: curvy_table Uses simple ASCII characters to produce a "round-edged" table appearance:: >>> from pibootctl.formatter import * >>> wrapper = TableWrapper(width=80, **curvy_table) >>> data = [ ... ('Name', 'Length', 'Position'), ... ('foo', 3, 1), ... ('bar', 3, 2), ... ('baz', 3, 3), ... ('quux', 4, 4)] >>> print(wrapper.fill(data)) ,------+--------+----------. | Name | Length | Position | |------+--------+----------| | foo | 3 | 1 | | bar | 3 | 2 | | baz | 3 | 3 | | quux | 4 | 4 | `------+--------+----------' .. data:: unicode_table Uses unicode box-drawing characters to produce a typical "box-like" table appearance:: >>> from pibootctl.formatter import * >>> wrapper = TableWrapper(width=80, **unicode_table) >>> data = [ ... ('Name', 'Length', 'Position'), ... ('foo', 3, 1), ... ('bar', 3, 2), ... ('baz', 3, 3), ... ('quux', 4, 4)] >>> print(wrapper.fill(data)) ┌──────┬────────┬──────────┠│ Name │ Length │ Position │ ├──────┼────────┼──────────┤ │ foo │ 3 │ 1 │ │ bar │ 3 │ 2 │ │ baz │ 3 │ 3 │ │ quux │ 4 │ 4 │ └──────┴────────┴──────────┘ .. data:: curvy_unicode_table Uses unicode box-drawing characters to produce a "round-edged" table appearance:: >>> from pibootctl.formatter import * >>> wrapper = TableWrapper(width=80, **curvy_unicode_table) >>> data = [ ... ('Name', 'Length', 'Position'), ... ('foo', 3, 1), ... ('bar', 3, 2), ... ('baz', 3, 3), ... ('quux', 4, 4)] >>> print(wrapper.fill(data)) ╭──────┬────────┬──────────╮ │ Name │ Length │ Position │ ├──────┼────────┼──────────┤ │ foo │ 3 │ 1 │ │ bar │ 3 │ 2 │ │ baz │ 3 │ 3 │ │ quux │ 4 │ 4 │ ╰──────┴────────┴──────────╯ .. autoclass:: TransMap .. autoclass:: FormatDict .. autofunction:: int_ranges .. autofunction:: render """ import re from bisect import bisect from textwrap import dedent, TextWrapper from itertools import islice, zip_longest, chain, tee class TableWrapper: """ Similar to :class:`~textwrap.TextWrapper`, this class provides facilities for wrapping text to a particular width, but with a focus on table-based output. The constructor takes numerous arguments, but typically you don't need to specify them all (or at all). A series of dictionaries are provided with "common" configurations: :data:`pretty_table`, :data:`curvy_table`, :data:`unicode_table`, and :data:`curvy_unicode_table`. For example:: >>> from pibootctl.formatter import * >>> wrapper = TableWrapper(width=80, **curvy_table) >>> data = [ ... ('Name', 'Length', 'Position'), ... ('foo', 3, 1), ... ('bar', 3, 2), ... ('baz', 3, 3), ... ('quux', 4, 4)] >>> print(wrapper.fill(data)) ,------+--------+----------. | Name | Length | Position | |------+--------+----------| | foo | 3 | 1 | | bar | 3 | 2 | | baz | 3 | 3 | | quux | 4 | 4 | `------+--------+----------' The :class:`TableWrapper` instance attributes (and keyword arguments to the constructor) are as follows: .. attribute:: width (default 70) The maximum number of characters that the table can take up horizontally. :class:`TableWrapper` guarantees that no output line will be longer than :attr:`width` characters. .. attribute:: header_rows (default 1) The number of rows at the top of the table that will be separated from the following rows by a horizontal border (:attr:`internal_line`). .. attribute:: footer_rows (default 0) The number of rows at the bottom of the table that will be separated from the preceding rows by a horizontal border (:attr:`internal_line`). .. attribute:: cell_separator (default ``' '``) The string used to separate columns of cells. .. attribute:: internal_line (default ``'-'``) The string used to draw horizontal lines inside the table for :attr:`header_rows` and :attr:`footer_rows`. .. attribute:: internal_separator (default ``' '``) The string used within runs of :attr:`internal_line` to separate columns. .. attribute:: borders (default ``('', '', '', '')``) A 4-tuple of strings which specify the characters used to create the left, top, right, and bottom borders of the table respectively. .. attribute:: corners (default ``('', '', '', '')``) A 4-tuple of strings which specify the characters used for the top-left, top-right, bottom-right, and bottom-left corners of the table respectively. .. attribute:: internal_borders (default ``('', '', '', '')``) A 4-tuple of strings which specify the characters used to interrupt runs of the :attr:`borders` characters to draw row and column separators. Like :attr:`borders` these are the left, top, right, and bottom characters respectively. .. attribute:: align A callable accepting three parameters: 0-based row index, 0-based column index, and the cell data. The callable must return a character indicating the intended alignment of data within the cell. "<" for left justification, "^" for centered alignment, and ">" for right justification (as in :meth:`str.format`). The default is to left align everything. .. attribute:: format A callable accepting three parameters: 0-based row index, 0-based column index, and the cell data. The callable must return the desired string representation of the cell data. The default simply calls :class:`str` on everything. :class:`TableWrapper` also provides similar public methods to :class:`~textwrap.TextWrapper`: .. automethod:: wrap .. automethod:: fill """ def __init__(self, width=70, header_rows=1, footer_rows=0, cell_separator=' ', internal_line='-', internal_separator=' ', borders=('', '', '', ''), corners=('', '', '', ''), internal_borders=('', '', '', ''), align=None, format=None): if len(borders) != 4: raise ValueError('borders must be a 4-tuple of strings') if len(corners) != 4: raise ValueError('corners must be a 4-tuple of strings') if len(internal_borders) != 4: raise ValueError('internal_borders must be a 4-tuple of strings') self.width = width self.header_rows = header_rows self.footer_rows = footer_rows self.internal_line = internal_line self.cell_separator = cell_separator self.internal_separator = internal_separator self.internal_borders = internal_borders self.borders = tuple(borders) self.corners = tuple(corners) self.internal_borders = tuple(internal_borders) if align is None: align = lambda row, col, data: '<' self.align = align if format is None: format = lambda row, col, data: str(data) self.format = format def fit_widths(self, widths): """ Internal method which, given the sequence of *widths* (the calculated maximum width of each column), reduces those widths until they fit in the specified :attr:`width` limit, taking into account the implied width of column separators, borders, etc. """ min_width = sum(( len(self.borders[0]), len(self.borders[2]), len(self.cell_separator) * (len(widths) - 1) )) # Minimum width of each column is 1 if min_width + len(widths) > self.width: raise ValueError('width is too thin to accommodate the table') total_width = sum(widths) + min_width # Reduce column widths until they fit in the available space. First, we # sort by the current column widths then by index so the widest columns # form a left-to-right ordered suffix of the list widths = sorted((w, i) for i, w in enumerate(widths)) while total_width > self.width: # Find the insertion point before the suffix suffix = bisect(widths, (widths[-1][0] - 1, -1)) suffix_len = len(widths) - suffix # Calculate the amount of width we still need to shed reduce_by = total_width - self.width if suffix > 0: # Limit this by the amount that can be removed evenly from the # suffix columns before the suffix needs to expand to encompass # more columns (requiring another loop) reduce_by = min( reduce_by, (widths[suffix][0] - widths[suffix - 1][0]) * suffix_len ) # Distribute the reduction evenly across the columns of the suffix widths[suffix:] = [ (w - reduce_by // suffix_len, i) for w, i in widths[suffix:] ] # Subtract the remainder from the left-most columns of the suffix for i in range(suffix, suffix + reduce_by % suffix_len): widths[i] = (widths[i][0] - 1, widths[i][1]) total_width -= reduce_by return [w for i, w in sorted((i, w) for w, i in widths)] def wrap_lines(self, data, widths): """ Internal method responsible for wrapping the contents of each cell in each row in *data* to the specified column *widths*. """ # Construct wrappers for each column width wrappers = [TextWrapper(width=width) for width in widths] for y, row in enumerate(data): aligns = [self.align(y, x, cell) for x, cell in enumerate(row)] # Construct a list of wrapped lines for each cell in the row; these # are not necessarily of equal length (hence zip_longest below) cols = [ wrapper.wrap(self.format(y, x, cell)) for x, (cell, wrapper) in enumerate(zip(row, wrappers)) ] for line in zip_longest(*cols, fillvalue=''): yield ( self.borders[0] + self.cell_separator.join( '{cell:{align}{width}}'.format( cell=cell, align=align, width=width) for align, width, cell in zip(aligns, widths, line)) + self.borders[2] ) def generate_lines(self, data): """ Internal method which, given a sequence of rows of tuples in *data*, uses :meth:`fit_widths` to calculate the maximum possible column widths, and :meth:`wrap_lines` to wrap the text in *data* to the calculated widths, yielding rows of strings to the caller. """ widths = [ max(1, max(len( self.format(y, x, item)) for x, item in enumerate(row))) for y, row in enumerate(zip(*data)) # transpose ] widths = self.fit_widths(widths) lines = iter(data) if self.borders[1]: yield ( self.corners[0] + self.internal_borders[1].join( self.borders[1] * width for width in widths) + self.corners[1] ) if self.header_rows > 0: yield from self.wrap_lines(islice(lines, self.header_rows), widths) yield ( self.internal_borders[0] + self.internal_separator.join( self.internal_line * w for w in widths) + self.internal_borders[2] ) yield from self.wrap_lines( islice(lines, len(data) - self.header_rows - self.footer_rows), widths) if self.footer_rows > 0: yield ( self.internal_borders[0] + self.internal_separator.join( self.internal_line * w for w in widths) + self.internal_borders[2] ) yield from self.wrap_lines(lines, widths) if self.borders[3]: yield ( self.corners[3] + self.internal_borders[3].join( self.borders[3] * width for width in widths) + self.corners[2] ) def wrap(self, data): """ Wraps the table *data* returning a list of output lines without final newlines. *data* must be a sequence of row tuples, each of which is assumed to be the same length. If the current :attr:`width` does not permit at least a single character per column (after taking account of the width of borders, internal separators, etc.) then :exc:`ValueError` will be raised. """ return list(self.generate_lines(data)) def fill(self, data): """ Wraps the table *data* returning a string containing the wrapped output. """ return '\n'.join(self.wrap(data)) # Some prettier defaults for TableWrapper pretty_table = { 'cell_separator': ' | ', 'internal_line': '-', 'internal_separator': '-+-', 'borders': ('| ', '-', ' |', '-'), 'corners': ('+-', '-+', '-+', '+-'), 'internal_borders': ('|-', '-+-', '-|', '-+-'), } curvy_table = pretty_table.copy() curvy_table['corners'] = (',-', '-.', "-'", '`-') unicode_table = { 'cell_separator': ' │ ', 'internal_line': '─', 'internal_separator': '─┼─', 'borders': ('│ ', '─', ' │', '─'), 'corners': ('┌─', '─â”', '─┘', '└─'), 'internal_borders': ('├─', '─┬─', '─┤', '─┴─'), } curvy_unicode_table = unicode_table.copy() curvy_unicode_table['corners'] = ('╭─', '─╮', '─╯', '╰─') def pairwise(iterable): """ Taken from the recipe in the documentation for :mod:`itertools`. """ a, b = tee(iterable) next(b, None) return zip(a, b) def int_ranges(values, range_sep='-', list_sep=', '): """ Given a set of integer *values*, returns a compressed string representation of all values in the set. For example: >>> int_ranges({1, 2}) '1, 2' >>> int_ranges({1, 2, 3}) '1-3' >>> int_ranges({1, 2, 3, 4, 8}) '1-4, 8' >>> int_ranges({1, 2, 3, 4, 8, 9}) '1-4, 8-9' *range_sep* and *list_sep* can be optionally specified to customize the strings used to separate ranges and lists of ranges respectively. """ if len(values) == 0: return '' elif len(values) == 1: return '{0}'.format(*values) elif len(values) == 2: return '{0}{sep}{1}'.format(*values, sep=list_sep) else: ranges = [] start = None for i, j in pairwise(sorted(values)): if start is None: start = i if j > i + 1: ranges.append((start, i)) start = j if j == i + 1: ranges.append((start, j)) else: ranges.append((j, j)) return list_sep.join( ('{start}{sep}{finish}' if finish > start else '{start}').format( start=start, finish=finish, sep=range_sep) for start, finish in ranges ) class TransTemplate(str): """ Used by :class:`TransMap` to transparently pass unknown format templates through for later substitution. When this value is used in a :meth:`str.format` substitution, it renders itself with the format specification as {self!conv:spec}, passing the template through verbatim. """ # NOTE: No calling str.format in this class! ;) def __repr__(self): return TransTemplate(self + '!r') def __str__(self): return TransTemplate(self + '!s') def __format__(self, spec): if spec: parts = ('{', self, ':', spec, '}') else: parts = ('{', self, '}') return ''.join(parts) class TransMap: """ Used with :meth:`str.format_map` to substitute only a subset of values in a given template, passing the rest through for later processing. For example: >>> '{foo}{bar}'.format_map(TransMap(foo=1)) '1{bar}' >>> '{foo:02d}{bar:02d}{baz:02d}'.format_map(TransMap(foo=1, baz=3)) '01{bar:02d}03' .. note:: One exception is that the ``!a`` conversion is not handled correctly. This is erroneously converted to ``!r``. Unfortunately there's no solution to this; it's a side-effect of the means by which the ``!a`` conversion is performed. """ def __init__(self, **kw): self._kw = kw def __contains__(self, key): return True def __getitem__(self, key): return self._kw.get(key, TransTemplate(key)) class FormatDict: """ Used to format *data*, a :class:`dict`, in a format acceptable as input to the :func:`render` function. The *key_title* and *value_title* strings provide the cells for the single header row. This class is intended to be used within a string for :meth:`str.format`. For example:: >>> from pibootctl.formatter import FormatDict >>> d = {'foo': 100, 'bar': 200} >>> print('An example table:\\n\\n{s}'.format(s=FormatDict(d))) An example table: | Key | Value | | foo | 100 | | bar | 200 | The format specification in the format string can be used to request different kinds of output, for instance:: >>> f = FormatDict({'foo': 100, 'bar': 200}) >>> print('An example list:\\n\\n{f:list}'.format(f=f)) An example list: * foo = 100 * bar = 200 >>> print('An example reference list:\\n\\n{f:refs}'.format(f=f)) An example reference list: [foo]: 100 [bar]: 200 The default format specification is "table", naturally. If the values are tuples that should be expanded into multiple columns, set *value_title* to a tuple with the corresponding column titles:: >>> from pibootctl.formatter import FormatDict >>> d = {'foo': (1, 100), 'bar': (2, 200)} >>> print('An example table:\\n\\n{s}'.format(s=FormatDict(d, ... value_title=('col1', 'col2')))) An example table: | Key | col1 | col2 | | foo | 1 | 100 | | bar | 2 | 200 | Tuple values are only supported for table output. .. note:: In Python versions before 3.7, you may need to use :class:`collections.OrderedDict` to ensure output of the elements of *data* in a particular order. Alternatively, you may specify a *sort_key* value which will be applied to the key values of the dict to sort them prior to output. """ def __init__(self, data, key_title='Key', value_title='Value', sort_key=None): self.data = data self.key_title = key_title self.value_title = value_title self.sort_key = sort_key def __format__(self, spec): if self.sort_key is None: items = self.data.items() else: items = ( (key, self.data[key]) for key in sorted(self.data.keys(), key=self.sort_key) ) if not spec or spec == 'table': if isinstance(self.value_title, tuple): return '\n'.join( '| {key} | {values} |'.format( key=key, values=' | '.join(values)) for key, values in chain( [(self.key_title, self.value_title)], items ) ) else: return '\n'.join( '| {key} | {value} |'.format(key=key, value=value) for key, value in chain( [(self.key_title, self.value_title)], items ) ) elif spec == 'list': return '\n'.join( '* {key} = {value}'.format(key=key, value=value) for key, value in items ) elif spec == 'refs': return '\n'.join( '[{key}]: {value}'.format(key=key, value=value) for key, value in items ) else: raise ValueError('Unknown format spec. {!r}'.format(spec)) def lex(text): """ Internal function which acts as the lexer for :func:`render`. """ row_re = re.compile(r'^\|.*\|$') item_re = re.compile(r'^\*') ref_re = re.compile(r'^\[[0-9A-Z]+\]:') for line in text.splitlines() + ['']: line = line.rstrip() if row_re.match(line): yield 'row', [col.strip() for col in line[1:-1].split('|')] elif item_re.match(line): yield 'item', line[1:].strip() elif ref_re.match(line): ref, link = line.split(':', 1) yield 'ref', (ref, link.strip()) elif line: yield 'line', line.strip() else: yield 'blank', None # Always yield a final "blank" just to make the outer parser easier yield 'blank', None def parse(text): """ Internal function which acts as the parser for :func:`render`. """ state = 'break' rows = [] items = [] item = [] para = [] def start_table(): nonlocal rows rows = [s] return 'table/row' def start_list(): nonlocal item, items item = [s] items = [] return 'list/item' def start_refs(): nonlocal items items = [s] return 'refs' def start_para(): nonlocal para para = [s] return 'para' def start_break(): return 'break' switch = { 'row': start_table, 'item': start_list, 'ref': start_refs, 'line': start_para, 'blank': start_break, } try: for token, s in lex(text): if state == 'break': state = switch[token]() elif state == 'table/row': if token == 'row': rows.append(s) else: yield 'table', rows state = switch[token]() elif state == 'list/item': if token == 'line': item.append(s) else: items.append(' '.join(item)) if token == 'item': item = [s] elif token == 'blank': state = 'list' else: yield 'list', items state = switch[token]() elif state == 'list': if token == 'item': state = 'list/item' item = [s] else: yield 'list', items state = switch[token]() elif state == 'refs': if token == 'ref': items.append(s) else: yield 'refs', items state = switch[token]() elif state == 'para': if token == 'line': para.append(s) else: yield 'para', ' '.join(para) state = switch[token]() else: assert False, 'invalid state' except KeyError: assert False, 'invalid token' assert state == 'break' def render(text, width=70, list_space=False, table_style=None): """ A crude renderer for a crude markup language intended for formatting documentation for the console. The markup recognized by this routine is as follows: .. code-block:: text * Paragraphs must be separated by at least one blank line. They will be wrapped to *width*. * Items in bulleted lists must start with an asterisk. No list nesting is permitted, but items may span several lines (without blank lines between them). Items will be wrapped to *width* and indented appropriately. * Lines beginning and ending with a pipe character are assumed to be table rows. Pipe characters also delimit columns within the row. The first row is assumed to be a header row and will be separated from the rest. An example table is shown below: | Command | Description | | cd | changes the current directory | | ls | lists the content of a directory | | cp | copies files | | mv | renames files | | rm | removes files | """ if table_style is None: table_style = {} para_wrapper = TextWrapper(width=width) list_wrapper = TextWrapper(width=width, initial_indent='* ', subsequent_indent=' ') table_wrapper = TableWrapper(width=width, **table_style) chunks = [] for token, data in parse(dedent(text)): if token == 'para': chunks.append(para_wrapper.fill(data)) elif token == 'list': if list_space: for item in data: chunks.append(list_wrapper.fill(item)) else: chunks.append('\n'.join( list_wrapper.fill(item) for item in data )) elif token == 'refs': ref_len = max(len(ref) for ref, link in data) chunks.append('\n'.join( para_wrapper.fill('{ref}:{space} {link}'.format( ref=ref, link=link, space=' ' * (ref_len - len(ref)))) for ref, link in data )) elif token == 'table': chunks.append(table_wrapper.fill(data)) else: assert False, 'invalid render state' return '\n\n'.join(chunks) pibootctl-0.5.2/pibootctl/info.py000066400000000000000000000117201372751746400170420ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2019, 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . """ The :mod:`pibootctl.info` module contains some simple routines for determining information about the Pi that the application is running on. .. autofunction:: get_board_revision .. autofunction:: get_board_serial .. autofunction:: get_board_type .. autofunction:: get_board_types .. autofunction:: get_board_mem """ import io import struct def _hexdump(filename, fmt='>L'): try: size = struct.calcsize(fmt) with io.open(filename, 'rb') as f: return struct.unpack(fmt, f.read(size))[0] except FileNotFoundError: return None def get_board_revision(): """ Return the Pi's board revision as an unsigned 32-bit integer number. This is the same number as reported under "Revision" in :file:`/proc/cpuinfo`. """ return _hexdump('/proc/device-tree/system/linux,revision') def get_board_serial(): """ Return the Pi's serial number as an unsigned 64-bit integer number. This can also be queried as "Serial" under :file:`/proc/cpuinfo`. """ return _hexdump('/proc/device-tree/system/linux,serial', '>Q') def get_board_type(): """ Return a string indicating the overall model of the Pi, e.g. "pi0w", "pi2", or "pi3+". This is derived from the result of :func:`get_board_revision` according to the Pi's `revision codes table`_. .. _revision codes table: https://www.raspberrypi.org/documentation/hardware/raspberrypi/revision-codes/README.md """ try: rev = get_board_revision() if rev is None: return None if rev & 0x800000: known_models = { 0x0: 'pi1', 0x1: 'pi1', 0x2: 'pi1', 0x3: 'pi1', 0x4: 'pi2', 0x5: 'pi1', 0x6: 'pi1', 0x8: 'pi3', 0x9: 'pi0', 0xa: 'pi3', 0xc: 'pi0w', 0xd: 'pi3+', 0xe: 'pi3+', 0x10: 'pi3+', 0x11: 'pi4', } model_id = rev >> 4 & 0xff try: return known_models[model_id] except KeyError: # Assume unknown IDs in excess of the maximum match the [pi4] # section for now if model_id > max(known_models.keys()): return 'pi4' else: raise else: # All old-style revs are pi1 models (A, B, A+, B+, CM1) return 'pi1' except KeyError: return None def get_board_types(): """ Return a set of strings used for matching the model of Pi against configuration sections according to the `conditional filters table`_. .. _conditional filters table: https://www.raspberrypi.org/documentation/configuration/config-txt/conditional.md """ return { None: set(), 'pi0': {'pi0'}, 'pi0w': {'pi0', 'pi0w'}, 'pi1': {'pi1'}, 'pi2': {'pi2'}, 'pi3': {'pi3'}, 'pi3+': {'pi3', 'pi3+'}, 'pi4': {'pi4'}, }[get_board_type()] def get_board_mem(): """ Return the amount of memory (in megabytes) present on the Pi, according to the model returned by :func:`get_board_revision`. """ try: rev = get_board_revision() if rev is None: return 0 if rev & 0x800000: return { 0: 256, 1: 512, 2: 1024, 3: 2048, 4: 4096, 5: 8192, }[rev >> 20 & 0x7] else: return { 0x0002: 256, 0x0003: 256, 0x0004: 256, 0x0005: 256, 0x0006: 256, 0x0007: 256, 0x0008: 256, 0x0009: 256, 0x0012: 256, 0x0015: 256, # sometimes 512 0x000d: 512, 0x000e: 512, 0x000f: 512, 0x0010: 512, 0x0011: 512, 0x0013: 512, 0x0014: 512, }[rev] except KeyError: return 0 def get_display_id(display=None): raise NotImplementedError pibootctl-0.5.2/pibootctl/main.py000066400000000000000000000766321372751746400170500ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . """ The :mod:`pibootctl.main` module defines the :class:`Application` class, and an instance of this called :data:`main`. Instances of :class:`Application` are callable and thus :data:`main` is the entry-point for the :doc:`pibootctl ` script. From an API perspective, this module is primarily useful for providing an instance of the :class:`~pibootctl.store.Store` class:: from pibootctl.main import main from pibootctl.store import Store, Current, Default store = main.store store[Current] = store['foo'] .. data:: main The instance of :class:`Application` which is the entry-point for the :doc:`pibootctl ` script. .. autoclass:: Application :members: """ import io import os import sys import gettext import argparse import configparser from datetime import datetime from pathlib import Path from . import __version__ from .setting import Command from .parser import BootConditions from .store import Store, Current, Default from .term import ErrorHandler, pager from .userstr import UserStr from .output import Output from .info import get_board_type, get_board_serial from .exc import InvalidConfiguration, IneffectiveConfiguration try: import argcomplete except ImportError: argcomplete = None _ = gettext.gettext class Application: """ An instance of this class (:data:`main`) is the entry point for the application. The instance is callable, accepting the command line arguments as its single (optional) argument. The arguments will be derived from :data:`sys.argv` if not provided:: >>> from pibootctl.main import main >>> try: ... main(['-h']) ... except SystemExit: ... pass usage: [-h] [--version] {help,?,status,dump,get,set,save,load,diff,show,cat,list,ls,remove,rm,rename,mv} ... .. warning:: Calling :data:`main` will raise :exc:`SystemExit` in several cases (usually when requesting help output). It will also replace the system exception hook (:func:`sys.excepthook`). This is intended and by design. If you wish to use :doc:`pibootctl ` as an API, you are better off investigating the :class:`~pibootctl.store.Store` class, or treating :doc:`pibootctl ` as a self-contained script and calling it with :mod:`subprocess`. """ def __init__(self): super().__init__() self._args = None self._commands = None self._config = None self._parser = None self._output = None self._store = None def __call__(self, args=None): if int(os.environ.get('_ARGCOMPLETE', '0')): if argcomplete: argcomplete.autocomplete(self.parser) else: raise RuntimeError('missing argcomplete') if not int(os.environ.get('DEBUG', '0')): sys.excepthook = ErrorHandler() sys.excepthook[InvalidConfiguration] = (self.invalid_config, 3) sys.excepthook[IneffectiveConfiguration] = (self.overridden_config, 4) sys.excepthook[PermissionError] = (self.permission_error, 6) sys.excepthook[Exception] = (sys.excepthook.exc_message, 1) with pager(): self._args = self.parser.parse_args(args) self._output = Output( self._args.style if 'style' in self._args else 'user') self._args.func() @property def config(self): """ Returns the script's configuration as derived from the files in the three pre-defined locations (see :doc:`pibootctl ` for more information). Returns a :class:`~argparse.Namespace` containing the parsed configuration. """ if self._config is None: self._config = self._get_config() return self._config @property def parser(self): """ The parser for all the sub-commands that the script accepts. The parser's defaults are derived from the configuration obtained from :attr:`config`. Returns the newly constructed argument parser. """ if self._parser is None: self._parser, self._commands = self._get_parser() return self._parser @property def commands(self): """ A dictionary mapping command names to their sub-parser. """ if self._commands is None: self._parser, self._commands = self._get_parser() return self._commands @property def store(self): """ The :class:`~pibootctl.store.Store` containing the current and stored boot configurations. """ if self._store is None: self._store = Store( self.config.boot_path, self.config.store_path, self.config.config_root, self.config.mutable_files, self.config.comment_lines) return self._store @staticmethod def _get_config(): parser = configparser.ConfigParser( defaults={ 'boot_path': '/boot', 'store_path': 'pibootctl', 'config_root': 'config.txt', 'mutable_files': 'config.txt', 'comment_lines': 'off', 'backup': 'on', 'package_name': 'pibootctl', 'reboot_required': '/var/run/reboot-required', 'reboot_required_pkgs': '/var/run/reboot-required.pkgs', }, empty_lines_in_values=False, default_section='defaults', delimiters=('=',), comment_prefixes=('#',), interpolation=None) read = parser.read( [ '/lib/pibootctl/pibootctl.conf', '/etc/pibootctl.conf', '{xdg_config}/pibootctl.conf'.format( xdg_config=os.environ.get( 'XDG_CONFIG_HOME', os.path.expanduser('~/.config'))), ], encoding='ascii') section = parser['defaults'] config = argparse.Namespace( boot_path=section['boot_path'], store_path=section['store_path'], config_root=section['config_root'], mutable_files=[ f.strip() for f in section['mutable_files'].splitlines()], backup=section.getboolean('backup'), comment_lines=section.getboolean('comment_lines'), package_name=section['package_name'], reboot_required=section['reboot_required'], reboot_required_pkgs=section['reboot_required_pkgs']) return config def _get_parser(self): parser = argparse.ArgumentParser( description=_( "%(prog)s is a tool for querying and modifying the boot " "configuration of the Raspberry Pi.")) parser.add_argument( '--version', action='version', version=__version__) parser.set_defaults(func=self.do_help) commands = parser.add_subparsers(title=_("commands")) help_cmd = commands.add_parser( "help", aliases=["?"], description=_( "With no arguments, displays the list of pibootctl " "commands. If a command name is given, displays the " "description and options for the named command. If a " "setting name is given, displays the description and " "default value for that setting."), help=_("Displays help about the specified command or setting")) help_cmd.add_argument( "cmd", metavar="command-or-setting", nargs='?', help=_( "The name of the command or setting to output help for") ).completer = self._complete_help help_cmd.set_defaults(func=self.do_help) dump_cmd = commands.add_parser( "status", aliases=["dump"], description=_( "Output the current value of modified boot time settings " "that match the specified pattern (or all if no pattern " "is provided)."), help=_("Output the current boot time configuration")) dump_cmd.add_argument( "vars", nargs="?", metavar="pattern", help=_( "If specified, only displays settings with names that " "match the specified pattern which may include shell " "globbing characters (e.g. *, ?, and simple [classes])") ).completer = self._complete_status dump_cmd.add_argument( "-a", "--all", action="store_true", help=_( "Include all settings, regardless of modification, in " "the output; by default, only settings which have been " "modified are included")) Output.add_style_arg(dump_cmd) dump_cmd.set_defaults(func=self.do_status) get_cmd = commands.add_parser( "get", description=_( "Query the status of one or more boot configuration " "settings. If a single setting is requested then just " "that value is output. If multiple values are requested " "then both setting names and values are output. This " "applies whether output is in the default, JSON, YAML, or " "shell-friendly styles."), help=_("Query the state of one or more boot settings")) get_cmd.add_argument( "get_vars", nargs="+", metavar="setting", help=_( "The name(s) of the setting(s) to query; if a single " "setting is given its value alone is output, if multiple " "settings are queried the names and values of the " "settings are output") ).completer = self._complete_get_vars Output.add_style_arg(get_cmd) get_cmd.set_defaults(func=self.do_get) set_cmd = commands.add_parser( "set", description=_( "Change the value of one or more boot configuration " "settings. To reset the value of a setting to its " "default, simply omit the new value."), help=_("Change the state of one or more boot settings")) set_cmd.add_argument( "--no-backup", action="store_false", dest="backup", help=_( "Don't take an automatic backup of the current boot " "configuration if one doesn't exist")) group = set_cmd.add_mutually_exclusive_group(required=False) group.add_argument( "--all", dest="context", action="store_const", const="all", help=_( "Set the specified settings on all Pis this SD card is used " "with. This is the default context.")) group.add_argument( "--this-model", dest="context", action="store_const", const="model", help=_( "Set the specified settings for this model of Pi only.")) group.add_argument( "--this-serial", dest="context", action="store_const", const="serial", help=_( "Set the specified settings for this Pi's serial number " "only.")) # TODO Finish --this-display #group.add_argument( # "--this-display", dest="context", action="store_const", # const="edid", help=_( # "Set the specified settings for the EDID of the specified " # "monitor only.")) group = Output.add_style_arg(set_cmd, required=True) group.add_argument( "set_vars", nargs="*", metavar="name=[value]", default=[], help=_( "Specify one or more settings to change on the command " "line; to reset a setting to its default omit the value") ).completer = self._complete_set_vars set_cmd.set_defaults(func=self.do_set, backup=True, context="all") save_cmd = commands.add_parser( "save", description=_( "Store the current boot configuration under a given " "name."), help=_("Store the current boot configuration for later use")) save_cmd.add_argument( "name", help=_( "The name to save the current boot configuration under; " "can include any characters legal in a filename") ).completer = self._complete_save_name save_cmd.add_argument( "-f", "--force", action="store_true", help=_( "Overwrite an existing configuration, if one exists")) save_cmd.set_defaults(func=self.do_save) load_cmd = commands.add_parser( "load", description=_( "Overwrite the current boot configuration with a stored " "one."), help=_("Replace the boot configuration with a saved one")) load_cmd.add_argument( "name", help=_("The name of the boot configuration to restore") ).completer = self._complete_load_name load_cmd.add_argument( "--no-backup", action="store_false", dest="backup", help=_( "Don't take an automatic backup of the current boot " "configuration if one doesn't exist")) load_cmd.set_defaults(func=self.do_load) diff_cmd = commands.add_parser( "diff", description=_( "Display the settings that differ between two stored boot " "configurations, or between one stored boot configuration " "and the current configuration."), help=_("Show the differences between boot configurations")) diff_cmd.add_argument( "left", nargs="?", default=Current, help=_( "The boot configuration to compare from, or the current " "configuration if omitted") ).completer = self._complete_diff_left diff_cmd.add_argument( "right", help=_("The boot configuration to compare against") ).completer = self._complete_diff_right Output.add_style_arg(diff_cmd) diff_cmd.set_defaults(func=self.do_diff) show_cmd = commands.add_parser( "show", aliases=["cat"], description=_( "Display the specified stored boot configuration, or the " "sub-set of its settings that match the specified " "pattern."), help=_("Show the specified stored configuration")) show_cmd.add_argument( "name", help=_("The name of the boot configuration to display") ).completer = self._complete_show_name show_cmd.add_argument( "vars", nargs="?", metavar="pattern", help=_( "If specified, only displays settings with names that " "match the specified pattern which may include shell " "globbing characters (e.g. *, ?, and simple [classes])") ).completer = self._complete_show_vars show_cmd.add_argument( "-a", "--all", action="store_true", help=_( "Include all settings, regardless of modification, in " "the output; by default, only settings which have been " "modified are included")) Output.add_style_arg(show_cmd) show_cmd.set_defaults(func=self.do_show) ls_cmd = commands.add_parser( "list", aliases=["ls"], description=_("List all stored boot configurations."), help=_("List the stored boot configurations")) Output.add_style_arg(ls_cmd) ls_cmd.set_defaults(func=self.do_list) rm_cmd = commands.add_parser( "remove", aliases=["rm"], description=_("Remove a stored boot configuration."), help=_("Remove a stored boot configuration")) rm_cmd.add_argument( "name", help=_("The name of the boot configuration to remove") ).completer = self._complete_remove_name rm_cmd.add_argument( "-f", "--force", action="store_true", help=_( "Ignore errors if the named configuration does not exist")) rm_cmd.set_defaults(func=self.do_remove) mv_cmd = commands.add_parser( "rename", aliases=["mv"], description=_("Rename a stored boot configuration."), help=_("Rename a stored boot configuration")) mv_cmd.add_argument( "name", help=_("The name of the boot configuration to rename") ).completer = self._complete_rename_name mv_cmd.add_argument( "to", help=_("The new name of the boot configuration") ).completer = self._complete_rename_to mv_cmd.add_argument( "-f", "--force", action="store_true", help=_( "Overwrite the target configuration, if it exists")) mv_cmd.set_defaults(func=self.do_rename) return parser, commands.choices @staticmethod def invalid_config(*exc): """ Generates the error message for unhandled :exc:`~pibootctl.exc.InvalidConfiguration` exceptions. These are caused when a configuration fails to validate, and have an :attr:`~pibootctl.exc.InvalidConfiguration.errors` attribute listing all the exceptions that occurred during validation. """ msg = sys.excepthook.exc_message(*exc) for error in exc[1].errors.values(): msg.extend(sys.excepthook.exc_message(type(error), error, None)) return msg @staticmethod def overridden_config(*exc): """ Generates the error message for unhandled :exc:`~pibootctl.exc.IneffectiveConfiguration` exceptions. These are caused when a boot configuration is split across multiple files; the application is permitted to modify a file before the final one, but a later file overrides a value the application has tried to set in the file it is permitted to modify. """ msg = sys.excepthook.exc_message(*exc) for expected, actual in exc[1].diff: if expected is None and actual is not None: template = _( "{actual.name} appears unexpectedly as {actual.value} in " "the generated configuration; please report this bug") elif expected is not None and actual is None: if expected.lines: template = _( "{expected.name} is not set in the generated " "configuration although it was set in " "{expected.lines[0].filename} line " "{expected.lines[0].linenum}; please report this bug") else: template = _( "{expected.name} is not set in the generated " "configuration; please report this bug") elif actual.lines: template = _( "Expected {expected.name} to be {expected.value}, but was " "{actual.value} after being overridden by " "{actual.lines[0].filename} line {actual.lines[0].linenum}") else: template = _( "Expected {expected.name} to be {expected.value}, but was " "{actual.value} with no valid lines; this usually means " "a setting like start_x or gpu_mem is in a file other " "than config.txt") msg.append(template.format(expected=expected, actual=actual)) return msg @staticmethod def permission_error(*exc): """ Generates the error message for unhandled :exc:`PermissionError` exceptions. As these are very likely to be caused by non-root execution, this is customzied to warn about this in the event that the effective UID is not 0. """ msg = sys.excepthook.exc_message(*exc) if os.geteuid() != 0: msg.append(_( "You need root permissions to modify the boot configuration or " "stored boot configurations")) return msg def _complete_configs(self, prefix): for config in self.store: if config is not Current and config is not Default: if config.startswith(prefix): yield config def _complete_settings(self, prefix, config_key): for setting in self.store[config_key].settings: if setting.startswith(prefix): yield setting def do_help(self): """ Implementation of the :doc:`help` command. """ default = self.store[Default].settings if 'cmd' in self._args and self._args.cmd is not None: if self._args.cmd in default: self._output.dump_setting(default[self._args.cmd], file=sys.stdout) raise SystemExit(0) if '.' in self._args.cmd: # TODO Mis-spelled setting; use something like levenshtein to # detect "close" but incorrect setting names raise ValueError(_( 'Unknown setting "{self._args.cmd}"').format(self=self)) if '_' in self._args.cmd: # Old-style command commands = [ setting for setting in default.values() if isinstance(setting, Command) and self._args.cmd in setting.commands ] if len(commands) == 0: raise ValueError(_( 'Unknown command "{self._args.cmd}"').format(self=self)) if len(commands) == 1: self._output.dump_setting(commands[0], file=sys.stdout) else: print(_( '{self._args.cmd} is affected by the following ' 'settings:\n\n' '{settings}').format( self=self, settings='\n'.join( setting.name for setting in commands))) raise SystemExit(0) self.parser.parse_args([self._args.cmd, '-h']) else: self.parser.parse_args(['-h']) def _complete_help(self, prefix, **kwargs): for command in self.commands: if command.startswith(prefix): yield command yield from self._complete_settings(prefix, Default) def do_status(self): """ Implementation of the :doc:`status` command. """ self._args.name = Current self.do_show() def _complete_status(self, prefix, **kwargs): yield from self._complete_settings(prefix, Current) def do_show(self): """ Implementation of the :doc:`show` command. """ settings = self.store[self._args.name].settings if self._args.vars: settings = settings.filter(self._args.vars) if not self._args.all: settings = settings.modified() self._output.dump_settings(settings, file=sys.stdout, mod_only=not self._args.all) def _complete_show_name(self, prefix, **kwargs): yield from self._complete_configs(prefix) def _complete_show_vars(self, prefix, parsed_args, **kwargs): yield from self._complete_settings(prefix, parsed_args.name) def do_get(self): """ Implementation of the :doc:`get` command. """ current = self.store[Current] if len(self._args.get_vars) == 1: try: print(self._output.format_value( current.settings[self._args.get_vars[0]].value)) except KeyError: raise ValueError(_( 'unknown setting: {}').format(self._args.get_vars[0])) else: settings = {} for var in self._args.get_vars: try: settings[var] = current.settings[var] except KeyError: raise ValueError(_('unknown setting: {}').format(var)) self._output.dump_settings(settings, file=sys.stdout) def _complete_get_vars(self, prefix, **kwargs): yield from self._complete_settings(prefix, Current) def do_set(self): """ Implementation of the :doc:`set` command. """ mutable = self.store[Current].mutable() if self._args.style == 'user': settings = {} for var in self._args.set_vars: if not '=' in var: raise ValueError(_('expected "=" in {}').format(var)) name, value = var.split('=', 1) settings[name] = UserStr(value) else: settings = self._output.load_settings(sys.stdin) context = { 'all': lambda: BootConditions(), 'model': lambda: BootConditions(pi=get_board_type()), 'serial': lambda: BootConditions(serial=get_board_serial()), 'display': lambda: BootConditions(display=get_display_id()), }[self._args.context]() mutable.update(settings, context) self.backup_if_needed() self.store[Current] = mutable self.mark_reboot_required() def _complete_set_vars(self, prefix, **kwargs): if '=' not in prefix: for setting in self._complete_settings(prefix, Current): yield setting + '=' def do_save(self): """ Implementation of the :doc:`save` command. """ try: self.store[self._args.name] = self.store[Current] except FileExistsError: if not self._args.force: raise del self.store[self._args.name] self.store[self._args.name] = self.store[Current] def _complete_save_name(self, prefix, parsed_args, **kwargs): if parsed_args.force: yield from self._complete_configs(prefix) def do_load(self): """ Implementation of the :doc:`load` command. """ # Look up the config to load before we do any backups, just in case the # user's made a mistake and the config doesn't exist to_load = self.store[self._args.name] self.backup_if_needed() self.store[Current] = to_load self.mark_reboot_required() def _complete_load_name(self, prefix, **kwargs): yield from self._complete_configs(prefix) def do_diff(self): """ Implementation of the :doc:`diff` command. """ # Keep references to the settings lying around while we dump the diff # as otherwise the settings lose their weak-ref during the dump left = self.store[self._args.left].settings right = self.store[self._args.right].settings self._output.dump_diff( self._args.left, self._args.right, left.diff(right), file=sys.stdout) def _complete_diff_left(self, prefix, **kwargs): yield from self._complete_configs(prefix) def _complete_diff_right(self, prefix, parsed_args, **kwargs): for name in self._complete_configs(prefix): if name != parsed_args.left: yield name def do_list(self): """ Implementation of the :doc:`list` command. """ current = self.store[Current] table = [ (key, value.hash == current.hash, value.timestamp) for key, value in self.store.items() if key not in (Current, Default) ] self._output.dump_store(table, file=sys.stdout) def do_remove(self): """ Implementation of the :doc:`remove` command. """ try: del self.store[self._args.name] except KeyError: if not self._args.force: raise FileNotFoundError(_( 'unknown configuration {}').format(self._args.name)) def _complete_remove_name(self, prefix, **kwargs): yield from self._complete_configs(prefix) def do_rename(self): """ Implementation of the :doc:`rename` command. """ try: self.store[self._args.to] = self.store[self._args.name] except FileExistsError: if not self._args.force: raise del self.store[self._args.to] self.store[self._args.to] = self.store[self._args.name] del self.store[self._args.name] def _complete_rename_name(self, prefix, **kwargs): yield from self._complete_configs(prefix) def _complete_rename_to(self, prefix, parsed_args, **kwargs): if parsed_args.force: for name in self._complete_configs(prefix): if name != parsed_args.name: yield name def backup_if_needed(self): """ Tests whether the active boot configuration is also present in the store (by checking for the calculated hash). If it isn't, constructs a unique filename (backup-) and saves a copy of the active boot configuration under it. """ if self.config.backup and self._args.backup and not self.store.active: name = 'backup-{now:%Y%m%d-%H%M%S}'.format(now=datetime.now()) suffix = 0 while True: try: self.store[name] = self.store[Current] except FileExistsError: # Pi's clocks can be very wrong when there's no network; # this just exists to guarantee that we won't try and # clobber an existing backup suffix += 1 name = 'backup-{now:%Y%m%d-%H%M%S}-{suffix}'.format( now=datetime.now(), suffix=suffix) else: print(_( 'Backed up current configuration in {name}').format( name=name), file=sys.stderr) break def mark_reboot_required(self): """ Writes the necessary files to indicate that the system requires a reboot. """ if self.config.reboot_required: with io.open(self.config.reboot_required, 'w') as f: f.write('*** ') f.write(_('System restart required')) f.write(' ***\n') if self.config.reboot_required_pkgs and self.config.package_name: with io.open(self.config.reboot_required_pkgs, 'a') as f: f.write(self.config.package_name) f.write('\n') main = Application() pibootctl-0.5.2/pibootctl/output.py000066400000000000000000000337211372751746400174540ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2019, 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . """ The :mod:`pibootctl.output` module defines the :class:`Output` class which is responsible for rendering various structures (the store list, diff output, etc.) in a selected style (JSON, YAML, user-friendly, etc.); it also provides a class method to add the supported styles to an :class:`~argparse.ArgumentParser`. .. autoclass:: Output :members: """ import io import json import shlex import gettext from operator import attrgetter, itemgetter import yaml from .store import Current from .setting import Command, OverlayParam from .formatter import TableWrapper, unicode_table, pretty_table, render from .term import term_size, term_is_utf8 _ = gettext.gettext def values(left, right): """ Utility function for JSON/YAML output; generates a dict containing *left* and *right* for non-:data:`None` values. """ obj = {} if left is not None: obj['left'] = left.value if right is not None: obj['right'] = right.value return obj class Output: """ A derivative of :class:`~argparse.Namespace` used by the main application, this class provides a variety of methods (:meth:`dump_store`, :meth:`dump_diff`, :meth:`load_settings`, etc.) for input and output of data. These methods change the format they work with based on the value of the :attr:`style` attribute which defaults to "user" (human-readable output), but can alternatively be "json", "yaml", or "shell", if the user specifies one of the style arguments which the :meth:`add_style_arg` can be used to create. """ def __init__(self, style='user', use_unicode=None): self.style = style if use_unicode is None: use_unicode = term_is_utf8() if use_unicode: self._table_style = unicode_table self._check_mark = '✓' else: self._table_style = pretty_table self._check_mark = 'x' @staticmethod def add_style_arg(parser, *, required=False): """ Create a mutually exclusive :mod:`argparse` group and add to it options for the various input and output styles supported by this class. """ parser.set_defaults(style="user") fmt_group = parser.add_mutually_exclusive_group(required=required) fmt_group.add_argument( "--json", dest="style", action="store_const", const="json", help=_("Use JSON as the format")) fmt_group.add_argument( "--yaml", dest="style", action="store_const", const="yaml", help=_("Use YAML as the format")) fmt_group.add_argument( "--shell", dest="style", action="store_const", const="shell", help=_("Use a var=value or tab-delimited format suitable for the " "shell")) return fmt_group def dump_store(self, store, file): """ Write the content of *store* (a sequence of (name, active, timestamp) triples) to the file-like object *file*. """ return { 'user': self._dump_store_user, 'shell': self._dump_store_shell, 'json': self._dump_store_json, 'yaml': self._dump_store_yaml, }[self.style](store, file) def _dump_store_user(self, store, file): if not store: file.write(_("No stored boot configurations found")) file.write("\n") else: self._print_table([ (_('Name'), _('Active'), _('Timestamp')) ] + [ (name, self._check_mark if active else '', timestamp.strftime('%Y-%m-%d %H:%M:%S')) for name, active, timestamp in sorted(store, key=itemgetter(0)) ], file) @staticmethod def _dump_store_json(store, file): json.dump([ {'name': name, 'active': active, 'timestamp': timestamp.isoformat()} for name, active, timestamp in store ], file) @staticmethod def _dump_store_yaml(store, file): yaml.dump([ {'name': name, 'active': active, 'timestamp': timestamp} for name, active, timestamp in store ], file) @staticmethod def _dump_store_shell(store, file): for name, active, timestamp in store: file.write('\t'.join( (timestamp.isoformat(), ('inactive', 'active')[active], name) )) file.write('\n') def dump_diff(self, left, right, diff, file): """ Write the *diff* (a sequence of (l, r) tuples in which l and r are either instances of :class:`Setting` or :data:`None`), of *left* and *right* (instances of :class:`Settings`) to the file-like object *file*. """ return { 'user': self._dump_diff_user, 'shell': self._dump_diff_shell, 'json': self._dump_diff_json, 'yaml': self._dump_diff_yaml, }[self.style](left, right, diff, file) def _dump_diff_user(self, left, right, diff, file): if not diff: file.write(_( "No differences between {left} and {right}").format( left='<{}>'.format(_('Current')) if left is Current else left, right=right)) file.write("\n") else: self._print_table([ (_('Name'), '<{}>'.format(_('Current')) if left is Current else left, right, ) ] + sorted([ (l.name if l is not None else r.name, '-' if l is None else self.format_setting_user(l), '-' if r is None else self.format_setting_user(r), ) for (l, r) in diff ]), file) @staticmethod def _dump_diff_json(left, right, diff, file): json.dump({ (l.name if l is not None else r.name): values(l, r) for (l, r) in diff }, file) @staticmethod def _dump_diff_yaml(left, right, diff, file): yaml.dump({ (l.name if l is not None else r.name): values(l, r) for (l, r) in diff }, file) @staticmethod def _dump_diff_shell(left, right, diff, file): file.write( ''.join( '\t'.join( (l.name if l is not None else r.name, '-' if l is None else Output._format_value_shell(l.value), '-' if r is None else Output._format_value_shell(r.value) ) ) + '\n' for l, r in diff )) def dump_settings(self, settings, file, mod_only=True): """ Write the content of *settings* (a :class:`Settings` instance or just a mapping of settings names to :class:`Setting` objects) to the file-like object *file*. """ return { 'user': self._dump_settings_user, 'shell': self._dump_settings_shell, 'json': self._dump_settings_json, 'yaml': self._dump_settings_yaml, }[self.style](settings, file, mod_only=mod_only) @staticmethod def _dump_settings_json(settings, file, mod_only=True): json.dump({ name: setting.value for name, setting in settings.items() }, file) @staticmethod def _dump_settings_yaml(settings, file, mod_only=True): yaml.dump({ name: setting.value for name, setting in settings.items() }, file) def _dump_settings_shell(self, settings, file, mod_only=True): for setting in settings.values(): file.write(self._format_setting_shell(setting)) file.write("\n") def _dump_settings_user(self, settings, file, mod_only=True): if not settings: file.write(_( "No modified settings matching the pattern found.\n")) if mod_only: file.write(_("Try --all to include unmodified settings.\n")) else: data = [ (_('Name'), _('Modified'), _('Value')) ] + [ ( setting.name, self._check_mark if setting.modified else '', self.format_setting_user(setting), ) for setting in sorted( settings.values(), key=attrgetter('name')) ] if mod_only: data = [(name, value) for (name, modified, value) in data] self._print_table(data, file) def load_settings(self, file): """ Load a dictionary of settings values from the file-like object *file*. """ return { 'user': self._load_settings_user, 'json': self._load_settings_json, 'yaml': self._load_settings_yaml, 'shell': self._load_settings_shell, }[self.style](file) @staticmethod def _load_settings_json(file): return json.load(file) @staticmethod def _load_settings_yaml(file): return yaml.load(file, Loader=yaml.SafeLoader) @staticmethod def _load_settings_shell(file): def parse(value): if value in {'false', 'true'}: return value == 'true' elif value.isdigit(): return int(value) elif value.startswith('(') and value.endswith(')'): return [ parse(shlex.quote(item)) for item in shlex.split(value[1:-1]) ] else: return shlex.split(value)[0] data = file.read().splitlines() if len(data) == 1: if '=' not in data[0]: return parse(data[0]) return { key.replace('_', '.'): parse(value) for line in data for key, value in (line.split('=', 1),) } @staticmethod def _load_settings_user(file): raise NotImplementedError def format_value(self, value): """ Return *value* (typically an :class:`int` or :class:`str`) formatted for output in the selected :attr:`style`. """ return { 'user': self._format_value_user, 'shell': self._format_value_shell, 'json': self._format_value_json, 'yaml': self._format_value_yaml, }[self.style](value) @staticmethod def _format_value_json(value): return json.dumps(value) @staticmethod def _format_value_yaml(value): with io.StringIO() as file: yaml.dump(value, file) return file.getvalue() @staticmethod def _format_value_shell(value): if value is None: return 'auto' elif isinstance(value, bool): return ('false', 'true')[value] elif isinstance(value, list): return '({})'.format(' '.join( Output._format_value_shell(e) for e in value )) else: return shlex.quote(str(value)) @staticmethod def _format_value_user(value): if value is None: return _('auto') elif isinstance(value, bool): return (_('off'), _('on'))[value] elif isinstance(value, str): return repr(value) else: return str(value) def _print_table(self, table, file): width = min(120, term_size()[0]) renderer = TableWrapper(width=width, **self._table_style) for line in renderer.wrap(table): file.write(line) file.write('\n') def dump_setting(self, setting, file): """ Output a help page describing the *setting* with information on the underlying configuration command or overlay, the default value, and a verbose description. """ assert self.style == 'user' width = min(120, term_size()[0]) fields = [ (_('Name'), setting.name), (_('Default'), self.format_setting_user(setting)), ] if isinstance(setting, Command): fields += [ (_('Command(s)'), ', '.join(setting.commands)) ] elif isinstance(setting, OverlayParam): fields += [ (_('Overlay'), setting.overlay), (_('Parameter'), setting.param), ] max_field_width = max(len(name) for name, value in fields) file.write('{fields}\n\n{doc}\n'.format( fields='\n'.join( '{name:>{width}}: {value}'.format( name=name, width=max_field_width, value=value) for name, value in fields ), doc=render(setting.doc, width=width, table_style=self._table_style), )) @staticmethod def _format_setting_shell(setting): return '{name}={value}'.format( name=setting.name.replace('.', '_'), value=Output._format_value_shell(setting.value) ) @staticmethod def format_setting_user(setting): """ Output the value of *setting* in "human readable" style, with the optional :attr:`~Setting.hint` in parentheses. """ value = Output._format_value_user(setting.value) return ( '{value}' if setting.hint is None else '{value} ({setting.hint})' ).format(value=value, setting=setting) pibootctl-0.5.2/pibootctl/parser.py000066400000000000000000000776021372751746400174160ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2019, 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . """ The :mod:`pibootctl.parser` module provides the :class:`BootParser` class for parsing the boot configuration of the Raspberry Pi. The output of this class consists of derivatives of :class:`BootLine` (:class:`BootSection`, :class:`BootCommand`, etc.) and :class:`BootFile` instances, which in turn reference :class:`BootConditions` instances to indicate the context in which they were found. .. autoclass:: BootParser :members: .. autoclass:: BootLine .. autoclass:: BootSection .. autoclass:: BootCommand .. autoclass:: BootInclude .. autoclass:: BootFile .. autoclass:: BootConditions :members: """ import io import os import hashlib import warnings from pathlib import Path from zipfile import ZipFile, ZipInfo from datetime import datetime from collections import namedtuple from .info import get_board_types, get_board_serial def coalesce(*values, default=None): for value in values: if value is not None: return value return default class BootInvalid(Warning): "Raised when an invalid line is encountered" class BootLine: """ Represents a line in a boot configuration. This is effectively an abstract base class and should never appear in output itself. Provides four attributes: .. attribute:: filename A :class:`str` indicating the path (relative to the configuration's root) of the file containing the line. .. attribute:: linenum The 1-based line number of the line. .. attribute:: conditions A :class:`BootConditions` specifying the filters in effect for this configuration line. .. attribute:: comment Any comment that appears after other content on the line, or :data:`None` if no comment was present """ def __init__(self, filename, linenum, conditions, comment=None): self.filename = filename self.linenum = linenum self.conditions = conditions self.comment = comment def compare(self, other): if not isinstance(other, BootLine): raise ValueError('other is not a BootLine') result = set() if self.filename == other.filename and self.linenum == other.linenum: result.add('location') if self.conditions == other.conditions: result.add('conditions') if self.comment == other.comment: result.add('comment') return result def __eq__(self, other): try: return self.compare(other) == { 'location', 'conditions', 'comment', 'key', 'value'} except ValueError: return NotImplemented class BootComment(BootLine): """ A derivative of :class:`BootLine` for lines consisting purely of ``# comments`` in a boot configuration. """ def compare(self, other): result = super().compare(other) if isinstance(other, BootComment): result |= {'key', 'value'} return result def __repr__(self): return ( 'BootComment(filename={self.filename!r}, linenum={self.linenum!r}, ' 'comment={self.comment!r})'.format(self=self)) class BootSection(BootLine): """ A derivative of :class:`BootLine` for ``[conditional sections]`` in a boot configuration. Adds a single attribute: .. attribute:: section The criteria of the section (everything between the square brackets). .. note:: The :attr:`conditions` for a :class:`BootSection` instance *includes* the filters defined by that section. """ def __init__(self, filename, linenum, conditions, section, comment=None): super().__init__(filename, linenum, conditions, comment) self.section = section def compare(self, other): result = super().compare(other) if isinstance(other, BootSection): result.add('key') if self.section == other.section: result.add('value') return result def __str__(self): return '[{self.section}]'.format(self=self) def __repr__(self): return ( 'BootSection(filename={self.filename!r}, linenum={self.linenum!r}, ' 'section={self.section!r})'.format(self=self)) class BootCommand(BootLine): """ A derivative of :class:`BootLine` which represents a command in a boot configuration, e.g. "disable_overscan=1". Adds several attributes: .. attribute:: command The title of the command; characters before the first "=" in the line. .. attribute:: params The value of the command; characters after the first "=" in the line. As a special case, the "initramfs" command has two values and thus if :attr:`command` is "initramfs" then this attribute will be a 2-tuple. .. attribute:: hdmi The HDMI display that the command applies to. This is usually :data:`None` unless the command has an explicit hdmi suffix (":" separated after the :attr:`command` title but before the "="), or the command appears in an [HDMI:1] section. """ def __init__(self, filename, linenum, conditions, command, params, hdmi=None, comment=None): super().__init__(filename, linenum, conditions, comment) self.command = command self.params = params self.hdmi = hdmi def compare(self, other): result = super().compare(other) if isinstance(other, BootCommand): if self.command == other.command and \ coalesce(self.hdmi, other.hdmi, 0) == \ coalesce(other.hdmi, self.hdmi, 0): result.add('key') if self.params == other.params: result.add('value') return result def __str__(self): if self.command == 'initramfs': template = '{self.command} {self.params[0]} {self.params[1]}' elif not self.hdmi: template = '{self.command}={self.params}' else: template = '{self.command}:{self.hdmi}={self.params}' return template.format(self=self) def __repr__(self): return ( 'BootCommand(filename={self.filename!r}, linenum={self.linenum!r}, ' 'command={self.command!r}, params={self.params!r}, ' 'hdmi={self.hdmi!r})'.format(self=self)) class BootInclude(BootLine): """ A derivative of :class:`BootLine` representing an "include" command in a boot configuration. Adds a single attribute: .. attribute:: include The name of the file to be included. """ def __init__(self, filename, linenum, conditions, include, comment=None): super().__init__(filename, linenum, conditions, comment) self.include = include def compare(self, other): result = super().compare(other) if isinstance(other, BootInclude): result.add('key') if self.include == other.include: result.add('value') return result def __str__(self): return 'include {self.include}'.format(self=self) def __repr__(self): return ( 'BootInclude(filename={self.filename!r}, linenum={self.linenum!r}, ' 'include={self.include!r})'.format(self=self)) class BootOverlay(BootLine): """ A derivative of :class:`BootLine` representing a device-tree overlay ("dtoverlay=") command in a boot configuration. Adds a single attribute: .. attribute:: overlay The name of the device-tree overlay to load. """ def __init__(self, filename, linenum, conditions, overlay, comment=None): super().__init__(filename, linenum, conditions, comment) self.overlay = overlay def compare(self, other): result = super().compare(other) if isinstance(other, BootOverlay): result.add('key') if self.overlay == other.overlay: result.add('value') return result def __str__(self): return 'dtoverlay={self.overlay}'.format(self=self) def __repr__(self): return ( 'BootOverlay(filename={self.filename!r}, linenum={self.linenum!r}, ' 'overlay={self.overlay!r})'.format(self=self)) class BootParam(BootLine): """ A derivative of :class:`BootLine` representing a parameter to a loaded device-tree overlay ("dtparam=") command in a boot configuration. Adds several attributes: .. attribute:: overlay The device-tree overlay that the parameter applies to. .. attribute:: param The name of the parameter affected by the command. .. attribute:: value The new value to assign to the overlay parameter. """ def __init__(self, filename, linenum, conditions, overlay, param, value, comment=None): super().__init__(filename, linenum, conditions, comment) self.overlay = overlay self.param = param self.value = value def compare(self, other): result = super().compare(other) if isinstance(other, BootParam): if self.overlay == other.overlay and self.param == other.param: result.add('key') if self.value == other.value: result.add('value') return result def __str__(self): return 'dtparam={self.param}={self.value}'.format(self=self) def __repr__(self): return ( 'BootParam(filename={self.filename!r}, linenum={self.linenum!r}, ' 'overlay={self.overlay!r}, param={self.param!r}, ' 'value={self.value!r})'.format(self=self)) class BootConditions(namedtuple('BootConditions', ( 'pi', 'hdmi', 'edid', 'serial', 'gpio', 'none', 'suppress_count' ))): """ Represents the set of conditional filters that apply to a given :class:`BootLine`. The class implements methods necessary to compare instances as if they were sets. For example:: >>> cond_all = BootConditions() >>> cond_pi3 = BootConditions(pi='pi3') >>> cond_pi3p = BootConditions(pi='pi3p') >>> cond_serial = BootConditions(pi='pi3', serial=0x12345678) >>> cond_all == cond_pi3 False >>> cond_all >= cond_pi3 True >>> cond_pi3 > cond_pi3p True >>> cond_serial < cond_pi3 True >>> cond_serial < cond_pi3p False .. attribute:: pi The model of pi that the section applies to. See `conditional filters`_ for details of valid values. This represents sections like ``[pi3]``. .. attribute:: hdmi The index of the HDMI port (0 or 1) that settings within this section will apply to, if no index-suffix is provided by the setting itself. This represents sections like ``[HDMI:0]``. .. attribute:: edid The EDID of the display that the section applies to. This represents sections like ``[EDID=VSC-TD2220]``. .. attribute:: serial The serial number of the Pi that settings within this section will apply to, stored as an :class:`int`. This represents sections like ``[0x12345678]``. .. attribute:: gpio The GPIO number and state that must be matched for settings in this section to apply, stored as a (gpio, state) tuple. This represents sections like ``[gpio2=0]``. .. attribute:: none If this is :data:`True` then a ``[none]`` section has been encountered and no settings apply. .. attribute:: suppress_count This is a "suppression count" used to track sections within included files that are currently disabled (because the include occurred within a section that itself is disabled). .. _conditional filters: https://www.raspberrypi.org/documentation/configuration/config-txt/conditional.md """ __slots__ = () def __new__(cls, pi=None, hdmi=None, edid=None, serial=None, gpio=None, none=False, suppress_count=0): return super().__new__( cls, pi, hdmi, edid, serial, gpio, none, suppress_count) def __eq__(self, other): if not isinstance(other, BootConditions): return NotImplemented return ( self.pi == other.pi and self.hdmi == other.hdmi and self.edid == other.edid and self.serial == other.serial and self.gpio == other.gpio and self.none == other.none # NOTE: suppress_count is deliberately excluded here; it is nothing # to do with the conditional filters themselves but is an artefact # of their effect on includes ) def __le__(self, other): if not isinstance(other, BootConditions): return NotImplemented return ( (self.pi == other.pi or other.pi is None or (self.pi, other.pi) in { ('pi3+', 'pi3'), ('pi0w', 'pi0'), }) and (self.hdmi == other.hdmi or other.hdmi is None) and (self.edid == other.edid or other.edid is None) and (self.serial == other.serial or other.serial is None) and (self.gpio == other.gpio or other.gpio is None) and (self.none == other.none or not other.none) # See note above regarding suppress_count ) def __ne__(self, other): return not (self == other) def __ge__(self, other): return not (self < other) def __lt__(self, other): return (self <= other) and (self != other) def __gt__(self, other): return (self >= other) and (self != other) def evaluate(self, section): """ Calculates a new conditional state (based upon the current conditional state) from the specified *section* criteria. Returns a new :class:`BootConditions` instance. """ # Derived from information at [COND] if section == 'all': return self._replace(pi=None, hdmi=None, edid=None, serial=None, gpio=None, none=False) elif section == 'none': return self._replace(none=True) elif section.startswith('HDMI:'): try: return self._replace(hdmi={ 'HDMI:0': 0, 'HDMI:1': 1, }[section]) except KeyError: # Ignore invalid filters (as the bootloader does) return self elif section.startswith('EDID='): return self._replace(edid=section[len('EDID='):]) elif section.startswith('gpio'): s = section[len('gpio'):] gpio, value = s.split('=', 1) try: gpio = int(gpio) value = bool(int(value)) except ValueError: return self else: return self._replace(gpio=(gpio, value)) elif section.startswith('0x'): try: return self._replace(serial=int(section, base=16)) except ValueError: return self elif section.startswith('pi'): if section in {'pi0', 'pi0w', 'pi1', 'pi2', 'pi3', 'pi3+', 'pi4'}: return self._replace(pi=section) else: return self else: warnings.warn( BootInvalid('unrecognized conditional: {}'.format(section))) return self assert False, 'invalid evaluate state' def generate(self, context=None): """ Given *context*, a :class:`BootConditions` instance representing the currently active conditional sections, this method yields the conditional secitons required to set the conditions to this instance. If *context* is not specified, it defaults to conditions equivalent to ``[any]``, which is the default in the Pi bootloader. For example:: >>> current = BootConditions(pi='pi2', gpio=(4, True)) >>> wanted = BootConditions() >>> print('\\n'.join(wanted.generate(current))) [all] >>> wanted = BootConditions(pi='pi4') >>> print('\\n'.join(wanted.generate(current))) [all] [pi4] >>> current = BootConditions(pi='pi2') >>> print('\\n'.join(wanted.generate(current))) [pi4] >>> current = BootConditions(none=True) >>> print('\\n'.join(wanted.generate(current))) [all] [pi3] .. note:: The yielded strings do *not* end with a line terminator. """ if context is None: context = BootConditions() # If we have to "undo" any conditionals (because the context conditions # limit gpio, for example but our conditions don't) then reset # everything with [all] if context.none or any( old is not None and new is None for old, new in zip(context[:-2], self[:-2])): yield '[all]' if self.pi is not None: yield '[{self.pi}]'.format(self=self) if self.hdmi is not None: yield '[HDMI:{self.hdmi}]'.format(self=self) if self.edid is not None: yield '[EDID={self.edid}]'.format(self=self) if self.serial is not None: yield '[0x{self.serial:X}]'.format(self=self) if self.gpio is not None: yield '[gpio{self.gpio[0]:d}={self.gpio[1]:d}]'.format(self=self) def suppress(self): """ If the current boot conditions are not :attr:`enabled`, returns a new :class:`BootConditions` instance with the suppression count incremented by one. This is used during parsing to disable all conditionals in suppressed includes. """ if not self.enabled: return self._replace(suppress_count=self.suppress_count + 1) else: return self @property def enabled(self): """ Returns :data:`True` if parsed items are currently effective. If this is :data:`False`, parsed items are ignored. """ return ( # Cannot currently assess HDMI, EDID, or GPIO criteria not self.none and (self.pi is None or self.pi in get_board_types()) and (self.serial is None or self.serial == get_board_serial()) and (self.suppress_count == 0) ) class BootFile(namedtuple('Content', ( 'filename', 'timestamp', 'content', 'encoding', 'errors' ))): """ Represents a file in a boot configuration. .. attribute:: filename A :class:`str` representing the file's path relative to the boot configuration's container (whatever that may be: a path, a zip archive, etc.) .. attribute:: timestamp A :class:`~datetime.datetime` containing the last modification timestamp of the file. .. note:: This is rounded down to a 2-second precision as that is all that `PKZIP`_ archives support. .. attribute:: content A :class:`bytes` string containing the complete content of the file. .. attribute:: encoding :data:`None` if the file is a binary file. Otherwise, specifies the name of the character encoding to be used when reading the file. .. attribute:: errors :data:`None` if the file is a binary file. Otherwise, specifies the character replacement strategy to be used with erroneous characters encountered when reading the file. .. _PKZIP: https://en.wikipedia.org/wiki/Zip_(file_format) """ __slots__ = () def __new__(cls, filename, timestamp, content, encoding=None, errors=None): # Adjust timestamps down to 2-second precision (all that's supported in # the PKZIP format), and to a minimum of 1980. This is to support those # scenarios (e.g. no network) in which a pi has de-synced clock and # winds up with files in 1970 (prior to the date PKZIP supports). return super().__new__( cls, filename, timestamp.replace( year=max(1980, timestamp.year), second=timestamp.second // 2 * 2, microsecond=0), content, encoding, errors) @classmethod def empty(cls, filename, encoding=None, errors=None): """ Class method for constructing an apparently empty :class:`BootFile`. """ return cls(filename, datetime(1970, 1, 1), b'', encoding, errors) def lines(self): """ Generator method which returns lines of text from the file using the associated :attr:`encoding` and :attr:`errors`. """ yield from io.TextIOWrapper( io.BytesIO(self.content), encoding=self.encoding, errors=self.errors) def add_to_zip(self, arc): """ Adds this :class:`BootFile` to the specified *arc* (which must be a :class:`~zipfile.ZipFile` instance), using the stored filename and last modification timestamp. """ info = ZipInfo(str(self.filename), ( self.timestamp.year, self.timestamp.month, self.timestamp.day, self.timestamp.hour, self.timestamp.minute, self.timestamp.second)) arc.writestr(info, self.content) class BootParser: """ Parser for the files used to configure the Raspberry Pi's bootloader. The *path* specifies the container of all files that make up the configuration. It be one of: * a :class:`str` or a :class:`~pathlib.Path` in which case the path specified must be a directory * a :class:`~zipfile.ZipFile` * a :class:`dict` mapping filenames to :class:`BootFile` instances; effectively the output of :attr:`files` after parsing """ def __init__(self, path): if isinstance(path, str): path = Path(path) assert isinstance(path, (Path, ZipFile, dict)) if isinstance(path, Path): assert path.is_dir() self._path = path self._files = {} self._hash = None self._config = None self._timestamp = None @property def path(self): """ The path under which all configuration files can be found. This may be a :class:`~pathlib.Path` instance, or a :class:`~zipfile.ZipFile`, or a :class:`dict`. """ return self._path @property def config(self): """ The parsed configuration; a sequence of :class:`BootLine` instances (or derivatives of :class:`BootLine`), after :meth:`parse` has been successfully called. """ return self._config @property def files(self): """ The content of all parsed files; a mapping of filename to :class:`BootFile` objects. """ return self._files @property def hash(self): """ After :meth:`parse` is successfully called, this is the SHA1 hash of the complete configuration in parsed order (i.e. starting at "config.txt" and proceeding through all included files). """ return self._hash.hexdigest().lower() @property def timestamp(self): """ The latest modified timestamp on all files that were read as a result of calling :meth:`parse`. """ return self._timestamp def parse(self, filename="config.txt"): """ Parse the boot configuration on :attr:`path`. The optional *filename* specifies the "root" of the configuration, and defaults to :file:`config.txt`. If parsing is successful, this will update the :attr:`files`, :attr:`hash`, :attr:`timestamp`, and :attr:`config` attributes. """ self._files.clear() self._hash = hashlib.sha1() self._timestamp = datetime(1970, 1, 1) # UNIX epoch self._config = list(self._parse(filename)) def add(self, filename, encoding=None, errors=None): """ Adds the auxilliary *filename* under :attr:`path` to the configuration. This is used to update the :attr:`hash` and :attr:`files` of the parsed configuration to include files which are referenced by the boot configuration but aren't themselves configuration files (e.g. EDID data, and the kernel cmdline.txt). If specified, *encoding* and *errors* are as for :func:`open`. If *encoding* is :data:`None`, the data is assumed to be binary and the method will return the content of the file as a :class:`bytes` string. Otherwise, the content of the file is assumed to be text and will be returned as a :class:`list` of :class:`str`. """ return self._open(filename, encoding, errors) def _parse(self, filename, conditions=None): overlay = 'base' if conditions is None: conditions = BootConditions() for linenum, content, comment in self._read_text(filename): if not content: yield BootComment(filename, linenum, conditions, comment) elif content.startswith('[') and content.endswith(']'): content = content[1:-1] conditions = conditions.evaluate(content) yield BootSection( filename, linenum, conditions, content, comment=comment) elif '=' in content: cmd, value = content.split('=', 1) # We deliberately don't strip cmd or value here because the # bootloader doesn't either; whitespace on either side of # the = is significant and can invalidate lines if cmd in {'device_tree_overlay', 'dtoverlay'}: if ':' in value: overlay, params = value.split(':', 1) yield BootOverlay( filename, linenum, conditions, overlay, comment=comment) for param, value in self._parse_params(overlay, params): yield BootParam( filename, linenum, conditions, overlay, param, value, comment=comment) else: overlay = value or 'base' yield BootOverlay( filename, linenum, conditions, overlay, comment=comment) elif cmd in {'device_tree_param', 'dtparam'}: for param, value in self._parse_params(overlay, value): yield BootParam( filename, linenum, conditions, overlay, param, value, comment=comment) else: if ':' in cmd: cmd, hdmi = cmd.split(':', 1) try: hdmi = int(hdmi) except ValueError: hdmi = None else: hdmi = conditions.hdmi yield BootCommand( filename, linenum, conditions, cmd, value, hdmi=hdmi, comment=comment) elif content.startswith('include'): command, included = content.split(None, 1) yield BootInclude(filename, linenum, conditions, included) yield from self._parse(included, conditions.suppress()) elif content.startswith('initramfs'): command, initrd, address = content.split(None, 2) yield BootCommand( filename, linenum, conditions, command, (initrd, address), comment=comment) else: warnings.warn(BootInvalid( "{filename}:{linenum} invalid line".format( filename=filename, linenum=linenum))) def _parse_params(self, overlay, params): for token in params.split(','): if '=' in token: param, value = token.split('=', 1) # Again, we deliberately don't strip param or value else: param = token value = 'on' if overlay == 'base': if param in {'i2c', 'i2c_arm', 'i2c1'}: param = 'i2c_arm' elif param in {'i2c_vc', 'i2c0'}: param = 'i2c_vc' elif param == 'i2c_baudrate': param = 'i2c_arm_baudrate' yield param, value def _read_text(self, filename): for linenum, line in enumerate( self._open(filename, encoding='ascii', errors='replace').lines(), start=1): # The bootloader ignores everything beyond column 80 and # leading whitespace. The following slicing and stripping of # the string is done in a precise order to ensure that we capture # any comments fully, but ignore all non-comment chars beyond # column 80 *before* stripping leading spaces try: i = line.index('#') except ValueError: comment = None else: line, comment = line[:i], line[i + 1:].rstrip() line = line.rstrip()[:80].lstrip() if not line.strip() and comment is None: continue yield linenum, line, comment def _open(self, filename, encoding=None, errors=None): if isinstance(self.path, Path): try: with (self.path / filename).open('rb') as f: file = BootFile( filename, datetime.fromtimestamp(os.fstat(f.fileno()).st_mtime), f.read(), encoding, errors) except FileNotFoundError: file = None elif isinstance(self.path, ZipFile): try: with self.path.open(str(filename), 'r') as f: file = BootFile( filename, datetime(*self.path.getinfo(f.name).date_time), f.read(), encoding, errors) except KeyError: # Yes, ZipFile raises KeyError when an archive member isn't # found... file = None elif isinstance(self.path, dict): try: file = BootFile( filename, self.path[filename].timestamp, self.path[filename].content, encoding, errors) except KeyError: file = None else: assert False, 'invalid path type' if file is None: # It is *not* an error if filename doesn't exist under path; e.g. # if config.txt doesn't exist that just means a purely default # config. Likewise, if edid.dat doesn't exist, that's normal. In # this case we return an "empty" file, but we *don't* add an entry # to files file = BootFile.empty(filename) else: self._timestamp = max(self._timestamp, file.timestamp) self._hash.update(file.content) self._files[filename] = file return file # [COND]: # https://www.raspberrypi.org/documentation/configuration/config-txt/conditional.md pibootctl-0.5.2/pibootctl/setting.py000066400000000000000000002611061372751746400175710ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2019, 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . """ The :mod:`pibootctl.setting` module defines all the classes used to represent boot configuration settings: .. image:: images/setting_hierarchy.* :align: center The base of the hierarchy is :class:`Setting` but this is effectively an abstract class and it is rare that anyone will need to use it directly. Rather you should derive from one of the concrete implementations below it like :class:`OverlayParam`, :class:`Command`, or one of the type-specializations like :class:`CommandBool`, :class:`CommandInt`, etc. .. note:: For the sake of brevity, only the generic classes defined in :mod:`pibootctl.setting` are documented here. There are also specialization classes specific to individual settings defined (for cases of complex inter-dependencies, e.g. how the Bluetooth enabled status affects the default serial UART). Developers are advised to familiarize themselves with the full range of classes in this module before defining additional ones. .. autoclass:: Setting :members: .. autoclass:: Overlay :members: overlay .. autoclass:: OverlayParam :members: param .. autoclass:: OverlayParamInt .. autoclass:: OverlayParamBool .. autoclass:: Command :members: commands, index .. autoclass:: CommandInt .. autoclass:: CommandIntHex .. autoclass:: CommandBool .. autoclass:: CommandBoolInv .. autoclass:: CommandForceIgnore :members: force, ignore .. autoclass:: CommandMaskMaster .. autoclass:: CommandMaskDummy .. autoclass:: CommandFilename :members: filename .. autoclass:: CommandIncludedFile .. autoexception:: ParseWarning .. autoexception:: ValueWarning """ import gettext import warnings from textwrap import dedent from functools import reduce from collections import namedtuple from itertools import groupby, chain from operator import or_, itemgetter from contextlib import contextmanager from .exc import DelegatedOutput from .formatter import FormatDict, TransMap, int_ranges from .parser import BootOverlay, BootParam, BootCommand, coalesce from .userstr import UserStr, to_bool, to_int, to_float, to_list, to_str from .info import get_board_type, get_board_mem _ = gettext.gettext def format_valid_table(doc, valid): """ A small utility function that replaces instances of ``{valid}`` in *doc* with a formatted table containing the keys and values of the :class:`dict` *valid*. """ return dedent(doc).format_map( TransMap(valid=FormatDict( valid, key_title=_('Value'), value_title=_('Meaning')))) class ParseWarning(Warning): """ Warning class used by :meth:`Setting.extract` to warn about invalid values while parsing. """ class ValueWarning(Warning): """ Warning class used by :meth:`Setting.validate` to warn about dangerous or inappropriate configurations. """ class Setting: """ Represents a configuration setting. Each setting has a *name* which uniquely identifies the setting, a *default* value, and an optional *doc* string. The life-cycle of a typical setting in the scenario where the active boot configuration is being changed is: * :meth:`extract` the value of a setting from parsed configuration lines * :meth:`update` the value of a setting from user-provided values * :meth:`validate` a setting in the wider context of a configuration * generate :meth:`output` to represent the setting in a new config.txt Optionally: * :attr:`hint` may be queried to describe a value in human-readable terms """ def __init__(self, name, *, default=None, doc=''): # self._settings is set in Settings.__init__ and Settings.copy self._settings = None self._name = name self._default = default self._value = None self._doc = dedent(doc).format(name=name, default=default) self._lines = () def __repr__(self): return ( '<{self.__class__.__name__} name={self.name!r} ' 'default={self.default!r} value={self.value!r} ' 'modified={self.modified}>'.format(self=self)) @property def name(self): """ The name of the setting. This is a dot-delimited list of strings; note that the individual components do not have to be valid identifiers. For example, "boot.kernel.64bit". """ return self._name @property def doc(self): """ A description of the setting, used as help-text on the command line. """ return self._doc @property def key(self): """ Returns a tuple of strings which will be used to order the output of :meth:`output` in the generated configuration. .. note:: The output of this property *must* be unique for each setting, unless a setting delegates all its output to another setting. """ raise NotImplementedError @property def modified(self): """ Returns :data:`True` when the setting has been modified. Note that it is *not* sufficient to simply compare :attr:`value` to :attr:`default` as some defaults are context- or platform-specific. """ return self._value is not None @property def default(self): """ The default value of this setting. The default may be sensitive to the wider context of :class:`~pibootctl.store.Settings` (i.e. the default of one setting can change depending on the current value of other settings). """ return self._default @property def value(self): """ Returns the current value of the setting (or the :attr:`default` if the setting has not been :attr:`modified`). """ # Must use self.default here, not self._default as descendents may # calculate more complex defaults return self.default if self._value is None else self._value @property def lines(self): """ Returns the :class:`~pibootctl.parser.BootLine` items which (if enabled by conditionals) affected the value of the setting, in the reverse order they were encountered while parsing (so the first *enabled* item holds the current value). """ return self._lines @property def hint(self): """ Provides a human-readable interpretation of the state of the setting. Used by the "dump" and "show" commands to provide translations of default and current values. Must return :data:`None` if no explanation is available or necessary. Otherwise, must return a :class:`str`. """ return None def extract(self, config): """ Given a *config* which must be a sequence of :class:`~pibootctl.parser.BootLine` items, yields each line that potentially affects the setting's value (including those currently disabled by conditionals), and the new value that the line produces (or :data:`None` indicating that the value is now, or is still, the default state). .. note:: This method is *not* influenced by conditionals that disable a line. In this case the method must still yield the line and the value it would produce (were it enabled). The caller will deal with the fact the line is currently disabled (but needs to be aware of such lines for the configuration mutator). For this reason (and others) this method must *not* affect :attr:`value` directly; the caller will handle mutating the value when required. """ raise NotImplementedError def update(self, value): """ Given a *value*, returns it transformed to the setting's native type (typically an :class:`int` or :class:`bool` but can be whatever type is appropriate). The *value* may be a regular type (:class:`str`, :class:`int`, :data:`None`, etc.) as deserialized from one of the input formats (JSON or YAML). Alternatively, it may be a :class:`~pibootctl.userstr.UserStr`, indicating that the value is a string given by the user on the command line and should be interpreted by the setting accordingly. .. note:: Note to implementers: the method must *not* affect :attr:`value` directly; the caller will handle this. """ return value def validate(self): """ Validates the setting within the context of the other :class:`~pibootctl.store.Settings`. Raises :exc:`ValueError` in the event that the current value is invalid. May optionally use :exc:`ValueWarning` to warn about dangerous or inappropriate configurations. """ def output(self): """ Yields lines of configuration to represent the current state of the setting (taking in account the context of other :class:`~pibootctl.store.Settings`). """ # If a setting's output is handled by another setting (e.g. for cases # where a single command is broken up into multiple settings), raise # DeletedOutput(master) where master is the setting that handles all # output for the subordinate settings. This is necessary to permit the # containing configuration to track which settings have actually # generated output (to avoid duplication of lines in such cases). raise NotImplementedError @contextmanager def _override(self, value): """ Used as a context manager, temporarily overrides the *value* of this setting until the contextual block ends. Note that *value* does **not** pass through :meth:`update` via this route. """ old_value = self._value self._value = value try: yield finally: self._value = old_value def _relative(self, path): """ Returns the name of this setting with a suffix replaced by *path*. The number of leading dot-characters in *path* dictate how many dot-separated components of this setting's name are removed before appending the remainder of *path*. For example: >>> s.name 'foo.bar' >>> s._relative('baz') 'foo.bar.baz' >>> s._relative('.baz') 'foo.baz' >>> s._relative('.baz.quux') 'foo.baz.quux' >>> s._relative('..baz.quux') 'baz.quux' In other words, a *path* with no dot-prefix returns children of the current setting, a *path* with a single dot-prefix returns siblings of the current setting, and so on. """ parts = self.name.split('.') while path[:1] == '.': del parts[-1] path = path[1:] path = path.split('.') return '.'.join(parts + path) def _query(self, name): """ Queries another setting in the same set as this one. This method should be used in preference to simply querying ``self._settings()[name]`` as it's possible that the named setting has been "hidden" in the set by a filter. This method bypasses the visible filter to ensure that settings can always query other settings. """ assert self._settings # This is set to a weakref.ref by the Settings initializer (and # Settings.copy); hence why we call it to return the actual reference. return self._settings()._items[name] class Overlay(Setting): """ Represents a boolean setting that is "on" if the represented *overlay* is present, and "off" otherwise. """ def __init__(self, name, *, overlay, default=False, doc=''): super().__init__(name, default=default, doc=doc) self._overlay = overlay @property def overlay(self): """ The name of the overlay this parameter affects. """ return self._overlay @property def key(self): return ('overlays', '' if self.overlay == 'base' else self.overlay) def extract(self, config): for item in config: if isinstance(item, BootOverlay): if item.overlay == self.overlay: yield item, True def update(self, value): return to_bool(value) def output(self): if self.value: yield 'dtoverlay={self.overlay}'.format(self=self) class OverlayParam(Overlay): """ Represents a *param* to a device-tree *overlay*. Like :class:`Setting`, this is effectively an abstract base class to be derived from. """ def __init__(self, name, *, overlay='base', param, default=None, doc=''): super().__init__(name, overlay=overlay, default=default, doc=doc) self._param = param @property def param(self): """ The name of the parameter within the base overlay that this setting represents. """ return self._param @property def key(self): return ( 'overlays', '' if self.overlay == 'base' else self.overlay, self.param ) def extract(self, config): value = None for item in config: if isinstance(item, BootOverlay): if item.overlay == self.overlay: yield item, value elif isinstance(item, BootParam): if item.overlay == self.overlay and item.param == self.param: value = item.value yield item, value def update(self, value): return value def output(self): # We don't worry about outputting the dtoverlay; presumably that is # represented by another setting and the key property will order our # output appropriately after the correct dtoverlay output if self.modified: yield 'dtparam={self.param}={self.value}'.format(self=self) class OverlayParamStr(OverlayParam): """ Represents a string parameter to a device-tree overlay. The *valid* parameter may optionally provide a dictionary mapping valid string values for the command to explanations, to be provided by the basic :attr:`~Setting.hint` implementation. """ def __init__(self, name, *, overlay='base', param, default=None, doc='', valid=None): if valid is None: valid = {} doc = format_valid_table(doc, valid) super().__init__(name, overlay=overlay, param=param, default=default, doc=doc) self._valid = valid @property def hint(self): return self._valid.get(self.value) def validate(self): if self._valid and self.value not in self._valid: raise ValueError(_( '{self.name} must be one of {valid}' ).format(self=self, valid=', '.join(self._valid))) class OverlayParamInt(OverlayParam): """ Represents an integer parameter to a device-tree overlay. The *valid* parameter may optionally provide a dictionary mapping valid integer values for the command to string explanations, to be provided by the basic :attr:`~Setting.hint` implementation. """ def __init__(self, name, *, overlay='base', param, default=0, doc='', valid=None): if valid is None: valid = {} doc = format_valid_table(doc, valid) super().__init__(name, overlay=overlay, param=param, default=default, doc=doc) self._valid = valid @property def hint(self): return self._valid.get(self.value) def extract(self, config): for item, value in super().extract(config): try: yield item, None if value is None else int(value) except ValueError: warnings.warn(ParseWarning( '{item.filename} line {item.linenum}: invalid integer ' '{value!r}'.format(item=item, value=value))) yield item, None def update(self, value): return to_int(super().update(value)) def validate(self): if self._valid and self.value not in self._valid: raise ValueError(_( '{self.name} must be in the range {valid}' ).format(self=self, valid=int_ranges(self._valid))) class OverlayParamBool(OverlayParam): """ Represents a boolean parameter to the base device-tree overlay. """ def __init__(self, name, *, overlay='base', param, default=False, doc=''): super().__init__(name, overlay=overlay, param=param, default=default, doc=doc) def extract(self, config): for item, value in super().extract(config): yield item, None if value is None else (value == 'on') def update(self, value): return to_bool(super().update(value)) def output(self): if self.modified: yield 'dtparam={self.param}={value}'.format( self=self, value='on' if self.value else 'off') class Command(Setting): """ Represents a string-valued configuration *command* or *commmands* (one of these must be specified, but not both). If multiple *commands* are represented, only the first will be generated by :meth:`output` in this base class. This is also the base class for most simple-valued configuration commands (integer, boolean, etc). """ def __init__(self, name, *, command=None, commands=None, default=None, doc='', index=None): assert (command is None) ^ (commands is None), \ 'command or commands must be given, not both' doc = dedent(doc).format_map(TransMap(index=index)) super().__init__(name, default=default, doc=doc) if command is None: self._commands = tuple(commands) else: self._commands = (command,) self._index = index @property def commands(self): """ The configuration commands that this setting represents. """ return self._commands @property def index(self): """ The index of this setting for multi-valued settings (e.g. settings which apply to HDMI outputs). """ return self._index @property def key(self): return ('commands', self.name) def extract(self, config): for item in config: if ( isinstance(item, BootCommand) and item.command in self.commands and coalesce(item.hdmi, 0) == coalesce(self.index, 0)): yield item, item.params def output(self, fmt=''): if self.modified: if self.index: template = '{self.commands[0]}:{self.index}={self.value:{fmt}}' else: template = '{self.commands[0]}={self.value:{fmt}}' yield template.format(self=self, fmt=fmt) class CommandStr(Command): """ Represents a string-valued configuration *command* or *commands*. The *valid* parameter may optionally provide a dictionary mapping valid string values for the command to explanations, to be provided by the basic :attr:`~Setting.hint` implementation. """ def __init__(self, name, *, command=None, commands=None, default=None, doc='', index=0, valid=None): if valid is None: valid = {} doc = format_valid_table(doc, valid) super().__init__(name, command=command, commands=commands, default=default, doc=doc, index=index) self._valid = valid @property def hint(self): return self._valid.get(self.value) def update(self, value): return to_str(value) def validate(self): if self._valid and self.value not in self._valid: raise ValueError(_( '{self.name} must be one of {valid}' ).format(self=self, valid=', '.join(self._valid))) class CommandInt(Command): """ Represents an integer-valued configuration *command* or *commands*. The *valid* parameter may optionally provide a dictionary mapping valid integer values for the command to string explanations, to be provided by the basic :attr:`~Setting.hint` implementation. """ def __init__(self, name, *, command=None, commands=None, default=0, doc='', index=0, valid=None): if valid is None: valid = {} doc = format_valid_table(doc, valid) super().__init__(name, command=command, commands=commands, default=default, doc=doc, index=index) self._valid = valid @property def hint(self): return self._valid.get(self.value) def extract(self, config): for item, value in super().extract(config): try: yield item, to_int(value) except ValueError: warnings.warn(ParseWarning( '{item.filename} line {item.linenum}: invalid integer ' '{value!r}'.format(item=item, value=value))) # Invalid integers get treated as the setting default (based on # what the bootloader does too - it *doesn't* ignore the # setting) yield item, None def update(self, value): return to_int(value) def validate(self): if self._valid and self.value not in self._valid: raise ValueError(_( '{self.name} must be in the range {valid}' ).format(self=self, valid=int_ranges(self._valid))) def output(self, fmt='d'): yield from super().output(fmt) class CommandIntHex(CommandInt): """ An integer-valued configuration *command* or *commands* that are typically represented in hexi-decimal (like memory addresses). """ @property def hint(self): return '{:#x}'.format(self.value) def output(self, fmt='#x'): yield from super().output(fmt) class CommandBool(Command): """ Represents a boolean-valued configuration *command* or *commands*. """ def __init__(self, name, *, command=None, commands=None, default=False, doc='', index=0): super().__init__(name, command=command, commands=commands, default=default, doc=doc, index=index) def extract(self, config): for item, value in super().extract(config): try: yield item, bool(to_int(value)) except ValueError: warnings.warn(ParseWarning( '{item.filename} line {item.linenum}: invalid bool ' '{value!r}'.format(item=item, value=value))) yield item, None def update(self, value): return to_bool(value) def output(self, fmt='d'): yield from super().output(fmt) class CommandBoolInv(CommandBool): """ Represents a boolean-valued configuration *command* or *commands* with inverted logic, e.g. video.overscan.enabled represents the ``disable_overscan`` setting and therefore its value is always the opposite of the actual written value. """ def extract(self, config): for item, value in super().extract(config): yield item, not value def output(self, fmt='d'): if self.modified: with self._override(not self.value): yield from super().output(fmt) class CommandForceIgnore(CommandBool): """ Represents the tri-valued configuration values with *force* and *ignore* commands, e.g. ``hdmi_force_hotplug`` and ``hdmi_ignore_hotplug``. For these cases, when both commands are "0" the setting is considered to have the value :data:`None` (which in most cases means "determine automatically"). When the *force* command is "1", the setting is :data:`True` and thus when the *ignore* command is "1", the setting is :data:`False`. When both are "1" (a contradictory setting) the final setting encountered takes precedence. """ def __init__(self, name, *, force, ignore, doc='', index=0): super().__init__(name, commands=(force, ignore), default=None, doc=doc) self._force = force self._ignore = ignore self._index = index @property def force(self): """ The boolean command that forces this setting on. """ return self._force @property def ignore(self): """ The boolean command that forces this setting off. """ return self._ignore def extract(self, config): value = None for item in config: try: if ( isinstance(item, BootCommand) and item.command in self.commands and int(item.params)): value = item.command == self.force yield item, value except ValueError: warnings.warn(ParseWarning( '{item.filename} line {item.linenum}: invalid integer ' '{item.params!r}'.format(item=item))) # In this case, the "value" of the command is effectively 0 # but because this setting is only affected by "positive" # commands there's no change. We yield the line to indicate # it *attempted* to affect the setting, but with the current # (internal) value so it doesn't yield item, value def output(self): if self.modified: if self.index: template = '{command}:{self.index}=1' else: template = '{command}=1' yield template.format( self=self, command={ True: self.force, False: self.ignore, }[self.value], ) class CommandMaskMaster(CommandInt): """ Represents an integer bit-mask setting as several settings. The "master" setting is the only one that produces any output. It defines the suffixes of its *dummies* (instances of :class:`CommandMaskDummy` which parse the same setting but produce no output of their own). The *mask* specifies the integer bit-mask to be applied to the underlying value for this setting. The right-shift will be calculated from this. Single-bit masks will be represented as boolean values rather than integers. """ def __init__(self, name, *, mask, command=None, commands=None, default=0, doc='', index=0, valid=None, dummies=()): assert mask super().__init__(name, command=command, commands=commands, default=default, doc=doc, index=index, valid=valid) self._mask = mask self._shift = (mask & -mask).bit_length() - 1 # ffs(3) self._bool = (mask >> self._shift) == 1 self._names = (self.name,) + tuple( self._relative(name) for name in dummies) def extract(self, config): for item, value in super().extract(config): value = (value & self._mask) >> self._shift yield item, bool(value) if self._bool else value def update(self, value): if self._bool: return to_bool(value) else: return super().update(value) def output(self): if any(self._query(name).modified for name in self._names): value = reduce(or_, ( self._query(name).value << self._query(name)._shift for name in self._names )) template = '{self.commands[0]}={value:#x}' yield template.format(self=self, value=value) class CommandMaskDummy(CommandMaskMaster): """ Represents portions of integer bit-masks which are subordinate to a :class:`CommandMaskMaster` setting. """ def output(self): # Override with appropriate DelegatedOutput in sub-classes if self.modified: raise DelegatedOutput('some.setting') else: return () class CommandFilename(Command): """ Represents settings that contain a filename affected by the os_prefix command. The :attr:`filename` returns the full filename incorporating the value of "boot.prefix" (if set), and :attr:`~Setting.hint` outputs a suitable message including the full path. """ @property def filename(self): """ The full filename represented by the value, after concatenating it with the value of "boot.prefix". """ return self._query('boot.prefix').value + self.value @property def hint(self): if self.value and self._query('boot.prefix').modified: return _('{!r} with boot.prefix').format(self.filename) else: return None class CommandIncludedFile(CommandFilename): """ Represents settings that reference a file which should be included in any stored boot configuration. """ # This class is effectively just a flag; the store handles scanning all # settings for descendents of this class and incorporating their content # after parsing the rest of the boot configuration class CommandDisplayGroup(CommandInt): """ Represents settings that control the group of display modes used for the configuration of a video output, e.g. ``hdmi_group`` or ``dpi_group``. """ def __init__(self, name, *, command=None, commands=None, default=0, doc='', index=0): super().__init__(name, command=command, commands=commands, default=default, doc=doc, index=index, valid={ 0: _('auto from EDID'), 1: 'CEA', 2: 'DMT', }) class DisplayMode(namedtuple('DisplayMode', ( 'resolution', 'refresh', 'ratio', 'notes'))): __slots__ = () def __new__(cls, resolution='', refresh='', ratio='', notes=''): return super().__new__(cls, resolution, refresh, ratio, notes) def __str__(self): if self.resolution: if self.notes: template = '{self.resolution} @{self.refresh} ({self.notes})' else: template = '{self.resolution} @{self.refresh}' else: template = '{self.notes}' return template.format(self=self) class CommandDisplayMode(CommandInt): """ Represents settings that control the mode of a video output, e.g. ``hdmi_mode`` or ``dpi_mode``. """ def __init__(self, name, *, command=None, commands=None, default=0, doc='', index=0): self._valid_cea = { 1: DisplayMode('640x480', '60Hz', '4:3', 'VGA'), 2: DisplayMode('480p', '60Hz', '4:3'), 3: DisplayMode('480p', '60Hz', '16:9'), 4: DisplayMode('720p', '60Hz', '16:9'), 5: DisplayMode('1080i', '60Hz', '16:9'), 6: DisplayMode('480i', '60Hz', '4:3'), 7: DisplayMode('480i', '60Hz', '16:9'), 8: DisplayMode('240p', '60Hz', '4:3'), 9: DisplayMode('240p', '60Hz', '16:9'), 10: DisplayMode('480i', '60Hz', '4:3', 'pixel quadrupling'), 11: DisplayMode('480i', '60Hz', '16:9', 'pixel quadrupling'), 12: DisplayMode('240p', '60Hz', '4:3', 'pixel quadrupling'), 13: DisplayMode('240p', '60Hz', '16:9', 'pixel quadrupling'), 14: DisplayMode('480p', '60Hz', '4:3', 'pixel doubling'), 15: DisplayMode('480p', '60Hz', '16:9', 'pixel doubling'), 16: DisplayMode('1080p', '60Hz', '16:9'), 17: DisplayMode('576p', '50Hz', '4:3'), 18: DisplayMode('576p', '50Hz', '16:9'), 19: DisplayMode('720p', '50Hz', '16:9'), 20: DisplayMode('1080i', '50Hz', '16:9'), 21: DisplayMode('576i', '50Hz', '4:3'), 22: DisplayMode('576i', '50Hz', '16:9'), 23: DisplayMode('288p', '50Hz', '4:3'), 24: DisplayMode('288p', '50Hz', '16:9'), 25: DisplayMode('576i', '50Hz', '4:3', 'pixel quadrupling'), 26: DisplayMode('576i', '50Hz', '16:9', 'pixel quadrupling'), 27: DisplayMode('288p', '50Hz', '4:3', 'pixel quadrupling'), 28: DisplayMode('288p', '50Hz', '16:9', 'pixel quadrupling'), 29: DisplayMode('576p', '50Hz', '4:3', 'pixel doubling'), 30: DisplayMode('576p', '50Hz', '16:9', 'pixel doubling'), 31: DisplayMode('1080p', '50Hz', '16:9'), 32: DisplayMode('1080p', '24Hz', '16:9'), 33: DisplayMode('1080p', '25Hz', '16:9'), 34: DisplayMode('1080p', '30Hz', '16:9'), 35: DisplayMode('480p', '60Hz', '4:3', 'pixel quadrupling'), 36: DisplayMode('480p', '60Hz', '16:9', 'pixel quadrupling'), 37: DisplayMode('576p', '50Hz', '4:3', 'pixel quadrupling'), 38: DisplayMode('576p', '50Hz', '16:9', 'pixel quadrupling'), 39: DisplayMode('1080i', '50Hz', '16:9', 'reduced blanking'), 40: DisplayMode('1080i', '100Hz', '16:9'), 41: DisplayMode('720p', '100Hz', '16:9'), 42: DisplayMode('576p', '100Hz', '4:3'), 43: DisplayMode('576p', '100Hz', '16:9'), 44: DisplayMode('576i', '100Hz', '4:3'), 45: DisplayMode('576i', '100Hz', '16:9'), 46: DisplayMode('1080i', '120Hz', '16:9'), 47: DisplayMode('720p', '120Hz', '16:9'), 48: DisplayMode('480p', '120Hz', '4:3'), 49: DisplayMode('480p', '120Hz', '16:9'), 50: DisplayMode('480i', '120Hz', '4:3'), 51: DisplayMode('480i', '120Hz', '16:9'), 52: DisplayMode('576p', '200Hz', '4:3'), 53: DisplayMode('576p', '200Hz', '16:9'), 54: DisplayMode('576i', '200Hz', '4:3'), 55: DisplayMode('576i', '200Hz', '16:9'), 56: DisplayMode('480p', '240Hz', '4:3'), 57: DisplayMode('480p', '240Hz', '16:9'), 58: DisplayMode('480i', '240Hz', '4:3'), 59: DisplayMode('480i', '240Hz', '16:9'), 60: DisplayMode('720p', '24Hz', '16:9'), 61: DisplayMode('720p', '25Hz', '16:9'), 62: DisplayMode('720p', '30Hz', '16:9'), 63: DisplayMode('1080p', '120Hz', '16:9'), 64: DisplayMode('1080p', '100Hz', '16:9'), 65: DisplayMode(notes='user timings'), 66: DisplayMode('720p', '25Hz', '64:27', 'Pi 4'), 67: DisplayMode('720p', '30Hz', '64:27', 'Pi 4'), 68: DisplayMode('720p', '50Hz', '64:27', 'Pi 4'), 69: DisplayMode('720p', '60Hz', '64:27', 'Pi 4'), 70: DisplayMode('720p', '100Hz', '64:27', 'Pi 4'), 71: DisplayMode('720p', '120Hz', '64:27', 'Pi 4'), 72: DisplayMode('1080p', '24Hz', '64:27', 'Pi 4'), 73: DisplayMode('1080p', '25Hz', '64:27', 'Pi 4'), 74: DisplayMode('1080p', '30Hz', '64:27', 'Pi 4'), 75: DisplayMode('1080p', '50Hz', '64:27', 'Pi 4'), 76: DisplayMode('1080p', '60Hz', '64:27', 'Pi 4'), 77: DisplayMode('1080p', '100Hz', '64:27', 'Pi 4'), 78: DisplayMode('1080p', '120Hz', '64:27', 'Pi 4'), 79: DisplayMode('1680x720', '24Hz', '64:27', 'Pi 4'), 80: DisplayMode('1680x720', '25Hz', '64:27', 'Pi 4'), 81: DisplayMode('1680x720', '30Hz', '64:27', 'Pi 4'), 82: DisplayMode('1680x720', '50Hz', '64:27', 'Pi 4'), 83: DisplayMode('1680x720', '60Hz', '64:27', 'Pi 4'), 84: DisplayMode('1680x720', '100Hz', '64:27', 'Pi 4'), 85: DisplayMode('1680x720', '120Hz', '64:27', 'Pi 4'), 86: DisplayMode('2560x720', '24Hz', '64:27', 'Pi 4'), 87: DisplayMode('2560x720', '25Hz', '64:27', 'Pi 4'), 88: DisplayMode('2560x720', '30Hz', '64:27', 'Pi 4'), 89: DisplayMode('2560x720', '50Hz', '64:27', 'Pi 4'), 90: DisplayMode('2560x720', '60Hz', '64:27', 'Pi 4'), 91: DisplayMode('2560x720', '100Hz', '64:27', 'Pi 4'), 92: DisplayMode('2560x720', '120Hz', '64:27', 'Pi 4'), 93: DisplayMode('2160p', '24Hz', '16:9', 'Pi 4'), 94: DisplayMode('2160p', '25Hz', '16:9', 'Pi 4'), 95: DisplayMode('2160p', '30Hz', '16:9', 'Pi 4'), 96: DisplayMode('2160p', '50Hz', '16:9', 'Pi 4'), 97: DisplayMode('2160p', '60Hz', '16:9', 'Pi 4'), 98: DisplayMode('4096x2160', '24Hz', '256:135', 'Pi 4'), 99: DisplayMode('4096x2160', '25Hz', '256:135', 'Pi 4'), 100: DisplayMode('4096x2160', '30Hz', '256:135', 'Pi 4'), 101: DisplayMode('4096x2160', '50Hz', '256:135', 'Pi 4'), 102: DisplayMode('4096x2160', '60Hz', '256:135', 'Pi 4'), 103: DisplayMode('2160p', '24Hz', '64:27', 'Pi 4'), 104: DisplayMode('2160p', '25Hz', '64:27', 'Pi 4'), 105: DisplayMode('2160p', '30Hz', '64:27', 'Pi 4'), 106: DisplayMode('2160p', '50Hz', '64:27', 'Pi 4'), 107: DisplayMode('2160p', '60Hz', '64:27', 'Pi 4'), } self._valid_dmt = { 1: DisplayMode('640x350', '85Hz', '64:35'), 2: DisplayMode('640x400', '85Hz', '16:10'), 3: DisplayMode('720x400', '85Hz', '18:10'), 4: DisplayMode('640x480', '60Hz', '4:3'), 5: DisplayMode('640x480', '72Hz', '4:3'), 6: DisplayMode('640x480', '75Hz', '4:3'), 7: DisplayMode('640x480', '85Hz', '4:3'), 8: DisplayMode('800x600', '56Hz', '4:3'), 9: DisplayMode('800x600', '60Hz', '4:3'), 10: DisplayMode('800x600', '72Hz', '4:3'), 11: DisplayMode('800x600', '75Hz', '4:3'), 12: DisplayMode('800x600', '85Hz', '4:3'), 13: DisplayMode('800x600', '120Hz', '4:3'), 14: DisplayMode('848x480', '60Hz', '16:9'), 15: DisplayMode('1024x768', '43Hz', '4:3', 'incompatible'), 16: DisplayMode('1024x768', '60Hz', '4:3'), 17: DisplayMode('1024x768', '70Hz', '4:3'), 18: DisplayMode('1024x768', '75Hz', '4:3'), 19: DisplayMode('1024x768', '85Hz', '4:3'), 20: DisplayMode('1024x768', '120Hz', '4:3'), 21: DisplayMode('1152x864', '75Hz', '4:3'), 22: DisplayMode('1280x768', '60Hz', '15:9', 'reduced blanking'), 23: DisplayMode('1280x768', '60Hz', '15:9'), 24: DisplayMode('1280x768', '75Hz', '15:9'), 25: DisplayMode('1280x768', '85Hz', '15:9'), 26: DisplayMode('1280x768', '120Hz', '15:9', 'reduced blanking'), 27: DisplayMode('1280x800', '60', '16:10', 'reduced blanking'), 28: DisplayMode('1280x800', '60Hz', '16:10'), 29: DisplayMode('1280x800', '75Hz', '16:10'), 30: DisplayMode('1280x800', '85Hz', '16:10'), 31: DisplayMode('1280x800', '120Hz', '16:10', 'reduced blanking'), 32: DisplayMode('1280x960', '60Hz', '4:3'), 33: DisplayMode('1280x960', '85Hz', '4:3'), 34: DisplayMode('1280x960', '120Hz', '4:3', 'reduced blanking'), 35: DisplayMode('1280x1024', '60Hz', '5:4'), 36: DisplayMode('1280x1024', '75Hz', '5:4'), 37: DisplayMode('1280x1024', '85Hz', '5:4'), 38: DisplayMode('1280x1024', '120Hz', '5:4', 'reduced blanking'), 39: DisplayMode('1360x768', '60Hz', '16:9'), 40: DisplayMode('1360x768', '120Hz', '16:9', 'reduced blanking'), 41: DisplayMode('1400x1050', '60Hz', '4:3', 'reduced blanking'), 42: DisplayMode('1400x1050', '60Hz', '4:3'), 43: DisplayMode('1400x1050', '75Hz', '4:3'), 44: DisplayMode('1400x1050', '85Hz', '4:3'), 45: DisplayMode('1400x1050', '120Hz', '4:3', 'reduced blanking'), 46: DisplayMode('1440x900', '60Hz', '16:10', 'reduced blanking'), 47: DisplayMode('1440x900', '60Hz', '16:10'), 48: DisplayMode('1440x900', '75Hz', '16:10'), 49: DisplayMode('1440x900', '85Hz', '16:10'), 50: DisplayMode('1440x900', '120Hz', '16:10', 'reduced blanking'), 51: DisplayMode('1600x1200', '60Hz', '4:3'), 52: DisplayMode('1600x1200', '65Hz', '4:3'), 53: DisplayMode('1600x1200', '70Hz', '4:3'), 54: DisplayMode('1600x1200', '75Hz', '4:3'), 55: DisplayMode('1600x1200', '85Hz', '4:3'), 56: DisplayMode('1600x1200', '120Hz', '4:3', 'reduced blanking'), 57: DisplayMode('1680x1050', '60Hz', '16:10', 'reduced blanking'), 58: DisplayMode('1680x1050', '60Hz', '16:10'), 59: DisplayMode('1680x1050', '75Hz', '16:10'), 60: DisplayMode('1680x1050', '85Hz', '16:10'), 61: DisplayMode('1680x1050', '120Hz', '16:10', 'reduced blanking'), 62: DisplayMode('1792x1344', '60Hz', '4:3'), 63: DisplayMode('1792x1344', '75Hz', '4:3'), 64: DisplayMode('1792x1344', '120Hz', '4:3', 'reduced blanking'), 65: DisplayMode('1856x1392', '60Hz', '4:3'), 66: DisplayMode('1856x1392', '75Hz', '4:3'), 67: DisplayMode('1856x1392', '120Hz', '4:3', 'reduced blanking'), 68: DisplayMode('1920x1200', '60Hz', '16:10', 'reduced blanking'), 69: DisplayMode('1920x1200', '60Hz', '16:10'), 70: DisplayMode('1920x1200', '75Hz', '16:10'), 71: DisplayMode('1920x1200', '85Hz', '16:10'), 72: DisplayMode('1920x1200', '120Hz', '16:10', 'reduced blanking'), 73: DisplayMode('1920x1440', '60Hz', '4:3'), 74: DisplayMode('1920x1440', '75Hz', '4:3'), 75: DisplayMode('1920x1440', '120Hz', '4:3', 'reduced blanking'), 76: DisplayMode('2560x1600', '60Hz', '16:10', 'reduced blanking'), 77: DisplayMode('2560x1600', '60Hz', '16:10'), 78: DisplayMode('2560x1600', '75Hz', '16:10'), 79: DisplayMode('2560x1600', '85Hz', '16:10'), 80: DisplayMode('2560x1600', '120Hz', '16:10', 'reduced blanking'), 81: DisplayMode('1366x768', '60Hz', '16:9'), 82: DisplayMode('1920x1080', '60Hz', '16:9', '1080p'), 83: DisplayMode('1600x900', '60Hz', '16:9', 'reduced blanking'), 84: DisplayMode('2048x1152', '60Hz', '16:9', 'reduced blanking'), 85: DisplayMode('1280x720', '60Hz', '16:9', '720p'), 86: DisplayMode('1366x768', '60Hz', '16:9', 'reduced blanking'), 87: DisplayMode(notes='user timings'), } doc = dedent(doc).format_map( TransMap( valid_cea=FormatDict( self._valid_cea, key_title=_('Mode'), value_title=(_('Resolution'), _('Refresh'), _('Ratio'), _('Notes'))), valid_dmt=FormatDict( self._valid_dmt, key_title=_('Mode'), value_title=(_('Resolution'), _('Refresh'), _('Ratio'), _('Notes'))), )) super().__init__(name, command=command, commands=commands, default=default, doc=doc, index=index) @property def hint(self): return { 0: _('auto from EDID'), 1: str(self._valid_cea.get(self.value, '?')), 2: str(self._valid_dmt.get(self.value, '?')), }.get(self._query(self._relative('.group')).value, '?') def validate(self): group = self._query(self._relative('.group')) valid = { 0: {0}, 1: self._valid_cea.keys(), 2: self._valid_dmt.keys(), }[group.value] if self.value not in valid: raise ValueError(_( '{self.name} must be {valid} when {group.name} is ' '{group.value}' ).format(self=self, valid=int_ranges(valid), group=group)) class CommandDisplayTimings(Command): """ Represents settings that manually specify the timings of a video output, e.g. ``hdmi_timings`` or ``dpi_timings``. """ def __init__(self, name, *, command=None, commands=None, default=None, doc='', index=0): super().__init__(name, command=command, commands=commands, default=[] if default is None else default, doc=doc, index=index) def extract(self, config): for item, value in super().extract(config): try: value = value.strip() if value: value = [to_int(elem) for elem in value.split()] yield item, value else: yield item, [] except ValueError: warnings.warn(ParseWarning( '{item.filename} line {item.linenum}: invalid integer in ' '{value!r}'.format(item=item, value=value))) yield item, [] def update(self, value): if isinstance(value, UserStr): value = value.strip() if value: return [int(elem) for elem in value.split(',')] return None else: return value def validate(self): if self.modified and len(self.value) not in (0, 17): raise ValueError( _('{self.name} takes 17 comma-separated integers') .format(self=self)) def output(self): if self.modified: joined_value = ' '.join(str(i) for i in self.value) with self._override(joined_value): yield from super().output() class CommandDisplayRotate(CommandInt): """ Represents settings that control the rotation of a video output. This is expected to work in concert with a :class:`CommandDisplayFlip` setting (both rotate and flip are usually conflated into a single command, e.g. ``display_hdmi_rotate`` or ``display_lcd_rotate``). Also handles the deprecated ``display_rotate`` command, and the extra ``lcd_rotate`` command. """ def __init__(self, name, *, command=None, commands=None, default=0, doc='', index=0): super().__init__(name, command=command, commands=commands, default=default, doc=doc, index=index) def extract(self, config): for item, value in super().extract(config): yield item, ((value & 0x3) * 90) def validate(self): if self.value not in (0, 90, 180, 270): raise ValueError(_( '{self.name} must be 0, 90, 180, or 270' ).format(self=self)) def output(self): flip = self._query(self._relative('.flip')) if self.modified or flip.modified: value = (self.value // 90) | (flip.value << 16) if 'lcd_rotate' in self.commands: # For the DSI LCD display, prefer lcd_rotate as it uses the # display's electronics to handle rotation rather than the GPU. # However, if a flip is required, just use the GPU (because we # have to anyway). if value > 0b11: template = '{self.commands[0]}={value:#x}' else: template = 'lcd_rotate={value}' else: if self.index: template = '{self.commands[0]}:{self.index}={value:#x}' else: template = '{self.commands[0]}={value:#x}' yield template.format(self=self, value=value) class CommandDisplayFlip(CommandInt): """ Represents settings that control reflection (flipping) of a video output. See :class:`CommandDisplayRotate` for further information. """ def __init__(self, name, *, command=None, commands=None, default=0, doc='', index=0): super().__init__(name, command=command, commands=commands, default=default, index=index, doc=doc, valid={ 0: 'none', 1: 'horizontal', 2: 'vertical', 3: 'both', }) def extract(self, config): for item, value in super().extract(config): yield item, ((value >> 16) & 0x3) def output(self): # See CommandDisplayRotate.output above if self.modified: raise DelegatedOutput(self._relative('.rotate')) else: return () class CommandDPIOutput(CommandMaskMaster): """ Represents the format portion of ``dpi_output_format``. """ def output(self): if self._query(self._relative('.enabled')).value: # For the DPI LCD display, always output dpi_output_format when # enable_dpi_lcd is set (and conversely, don't output it when not # set) yield from super().output() class CommandDPIDummy(CommandMaskDummy): """ Represents the non-format portions of ``dpi_output_format``. """ def output(self): if self.modified: raise DelegatedOutput('video.dpi.format') else: return () class CommandHDMIBoost(CommandInt): """ Represents the ``config_hdmi_boost`` setting with its custom range of valid values. """ def validate(self): if not 0 <= self.value <= 11: raise ValueError(_( '{self.name} must be between 0 and 11 (default 5)' ).format(self=self)) class CommandEDIDIgnore(CommandIntHex): """ Represents the ``hdmi_ignore_edid`` "boolean" setting with its bizarre "true" value. """ # See hdmi_ignore_edid in # https://www.raspberrypi.org/documentation/configuration/config-txt/video.md def __init__(self, name, *, command=None, commands=None, default=False, doc=''): super().__init__(name, command=command, commands=commands, default=default, doc=doc) @property def hint(self): pass def extract(self, config): for item, value in super().extract(config): yield item, value == 0xa5000080 def update(self, value): return to_bool(value) def output(self): if self.modified: new_value = 0xa5000080 if self.value else 0 with self._override(new_value): yield from super().output() class CommandBootDelay2(Command): """ Represents the combination of ``boot_delay`` and ``boot_delay_ms``. """ def extract(self, config): boot_delay = boot_delay_ms = 0 for item in config: if isinstance(item, BootCommand): if item.command == 'boot_delay': try: boot_delay = to_int(item.params) except ValueError: warnings.warn(ParseWarning( '{item.filename} line {item.linenum}: invalid ' 'integer {item.params!r}'.format(item=item))) boot_delay = 0 yield item, boot_delay + (boot_delay_ms / 1000) elif item.command == 'boot_delay_ms': try: boot_delay_ms = to_int(item.params) except ValueError: warnings.warn(ParseWarning( '{item.filename} line {item.linenum}: invalid ' 'integer {item.params!r}'.format(item=item))) boot_delay_ms = 0 yield item, boot_delay + (boot_delay_ms / 1000) def output(self): if self.modified: whole, frac = divmod(self.value, 1) whole = int(whole) frac = int(frac * 1000) if whole: yield 'boot_delay={value}'.format(value=whole) if frac: yield 'boot_delay_ms={value}'.format(value=frac) def update(self, value): return to_float(value) def validate(self): if self.value < 0.0: raise ValueError(_( '{self.name} cannot be negative' ).format(self=self)) class CommandKernelAddress(CommandIntHex): """ Represents the ``kernel_address`` setting, and implements its context-sensitive default values. Also handles the deprecated ``kernel_old`` configuration parameter. """ @property def default(self): if self._query(self._relative('.64bit')).value: return 0x80000 else: return 0x8000 def extract(self, config): for item in config: if isinstance(item, BootCommand): try: if item.command == 'kernel_address': yield item, to_int(item.params) elif item.command == 'kernel_old': # TODO What does kernel_old=0 mean? Similar to start_x=0? if to_int(item.params): yield item, 0 except ValueError: warnings.warn(ParseWarning( '{item.filename} line {item.linenum}: invalid integer ' '{item.params!r}'.format(item=item))) yield item, None class CommandKernel64(CommandBool): """ Handles the ``arm_64bit`` configuration setting, and the deprecated ``arm_control`` setting it replaced. """ def extract(self, config): for item in config: if isinstance(item, BootCommand): try: if item.command == 'arm_64bit': yield item, bool(to_int(item.params)) elif item.command == 'arm_control': yield item, bool(to_int(item.params) & 0x200) except ValueError: warnings.warn(ParseWarning( '{item.filename} line {item.linenum}: invalid integer ' '{item.params!r}'.format(item=item))) yield item, None class CommandKernelFilename(CommandFilename): """ Handles the ``kernel`` setting and its platform-dependent defaults. """ @property def default(self): if self._query(self._relative('.64bit')).value: return 'kernel8.img' else: board_type = get_board_type() if board_type == 'pi4': return 'kernel7l.img' elif board_type in {'pi2', 'pi3', 'pi3+'}: return 'kernel7.img' else: return 'kernel.img' class CommandKernelCmdline(CommandIncludedFile): """ Handles the ``cmdline`` setting. """ # TODO modification/tracking of external file Firmware = namedtuple('Firmware', ('default', 'camera', 'debug', 'cutdown')) FW_START = { # pi4: default camera debug(+camera) lite False: Firmware('start.elf', 'start_x.elf', 'start_db.elf', 'start_cd.elf'), True: Firmware('start4.elf', 'start4x.elf', 'start4db.elf', 'start4cd.elf'), } FW_FIXUP = { key: Firmware(*( filename.replace('start', 'fixup').replace('.elf', '.dat') for filename in filenames )) for key, filenames in FW_START.items() } # Some notes on start_x, start_debug, start_file, and fixup_file for the # setting classes below. # # The interaction between these settings is both simple and horribly # complicated (at least from the perspective of this application). Each of # these is effectively a separate setting within the firmware, and is evaluated # in order, so only the final value of each setting matters. However, there are # rules of precedence regarding those final values: # # 1. non-blank start_file and fixup_file values trump everything; if these are # set they are acted upon. # 2. if start_file and fixup_file aren't set then gpu_mem=16 wins next; if this # is set then start_file is effectively "start_cd.elf" ("start4cd.elf" on # the pi4) # 3. Otherwise, start_debug=1 wins; if this is set then start_file is # effectively "start_db.elf" ("start4db.elf" on the pi4), and fixup_file is # "fixup_db.dat" or "fixup4db.dat" # 4. Otherwise, start_x=1 wins; if this is set then start_file is "start_x.elf" # ("start4x.elf" on the pi4), and fixup_file is "fixup_x.dat" or # "fixup4x.dat". # 5. If no values are specified for these, then start_file is effectively # "start.elf" or ("start4.elf" on the pi4) and fixup_file is "fixup.dat" (or # "fixup4.dat"). # 6. The debug firmware incorporates the camera firmware. # # Some consequences of the above rules; consider the following (silly, but # valid) configuration: # # start_debug=1 # start_x=1 # start_x=0 # # This results in the debug firmware being loaded because at the end of parsing # the configuration, start_file hasn't been explicitly set, gpu_mem defaults to # 64, and start_debug is 1 (so start_x is irrelevant; whether it's 0 or 1 the # debug firmware would be loaded). Likewise, consider: # # start_x=1 # start_debug=1 # start_debug=0 # # This results in the camera firmware being loaded as start_debug is 0 by the # end of parsing and start_x is still 1. In turn, this implies that the # "start_x=0" and "start_debug=0" states are fairly meaningless statements. If # a configuration explicitly sets them to zero (ultimately) we should simply # treat them as "unset". class CommandFirmwareCamera(CommandBool): """ Handles the ``start_x`` and ``start_debug`` settings. """ @property def default(self): pi4 = get_board_type() == 'pi4' return (self._query('gpu.mem').value >= 64) and ( self._query('boot.firmware.filename').value, self._query('boot.firmware.fixup').value ) in { (FW_START[pi4].camera, FW_FIXUP[pi4].camera), # The debug firmware includes the camera firmware, so start_debug # also implicitly activates the camera (FW_START[pi4].debug, FW_FIXUP[pi4].debug) } def extract(self, config): for item, value in super().extract(config): # NOTE: start_x is only valid in config.txt if item.filename == 'config.txt' and item.command == 'start_x': yield item, True if value else None def output(self): if self.modified and self.value: yield 'start_x=1' def validate(self): if self.value and self._query('gpu.mem').value < 64: raise ValueError(_( 'gpu.mem must be at least 64 when camera.enabled is on')) class CommandFirmwareDebug(CommandBool): """ Handles the ``start_debug`` setting. """ @property def default(self): pi4 = get_board_type() == 'pi4' return (self._query('gpu.mem').value > 16) and ( self._query('boot.firmware.filename').value, self._query('boot.firmware.fixup').value ) == (FW_START[pi4].debug, FW_FIXUP[pi4].debug) def extract(self, config): for item, value in super().extract(config): # NOTE: start_debug is only valid in config.txt if item.filename == 'config.txt' and item.command == 'start_debug': yield item, True if value else None def output(self): if self.modified and self.value: yield 'start_debug=1' class CommandFirmwareFilename(CommandFilename): """ Handles the ``start_file`` setting. """ @property def default(self): pi4 = get_board_type() == 'pi4' debug = self._query('boot.debug.enabled') camera = self._query('camera.enabled') # The "modified" tests below appear extraneous but aren't; they guard # against a circular reference in the case where everything is default. if self._query('gpu.mem').value <= 16: return FW_START[pi4].cutdown elif debug.modified and debug.value: return FW_START[pi4].debug elif camera.modified and camera.value: return FW_START[pi4].camera else: return FW_START[pi4].default def extract(self, config): for item, value in super().extract(config): # NOTE: start_filename is only valid in config.txt if item.filename == 'config.txt': yield item, value # TODO validate() to check for pi4/non-pi4 compatible firmware class CommandFirmwareFixup(CommandFilename): """ Handles the ``fixup_file`` setting. """ @property def default(self): pi4 = get_board_type() == 'pi4' debug = self._query('boot.debug.enabled') camera = self._query('camera.enabled') # See notes above if self._query('gpu.mem').value <= 16: return FW_FIXUP[pi4].cutdown elif debug.modified and debug.value: return FW_FIXUP[pi4].debug elif camera.modified and camera.value: return FW_FIXUP[pi4].camera else: return FW_FIXUP[pi4].default def extract(self, config): for item, value in super().extract(config): # NOTE: fixup_file is only valid in config.txt if item.filename == 'config.txt': yield item, value # TODO validate() to check for pi4/non-pi4 compatible firmware class CommandDeviceTree(CommandFilename): """ Handles the ``device_tree`` command. """ class CommandDeviceTreeAddress(CommandIntHex): """ Handles the ``device_tree_address`` command. """ @property def hint(self): if self.value == 0: return _('auto') else: return super().hint class CommandRamFSAddress(CommandIntHex): """ Handles the ``ramfsaddr`` and ``initramfs`` commands. """ @property def hint(self): if self.value == 0: return _('auto') # followkernel else: # FIXME What? return super().hint def extract(self, config): for item in config: if isinstance(item, BootCommand): try: if item.command == 'ramfsaddr': yield item, to_int(item.params) elif item.command == 'initramfs': filename, address = item.params if address == 'followkernel': yield item, None else: yield item, to_int(address) except ValueError: warnings.warn(ParseWarning( '{item.filename} line {item.linenum}: invalid integer ' '{item.params!r}'.format(item=item))) yield item, None def output(self): if self.value == 0: raise DelegatedOutput(self._relative('.filename')) else: yield from super().output() class CommandRamFSFilename(Command): """ Handles the ``ramfsfile`` and ``initramfs`` commands which can both accept multiple files (to be concatenated). """ def __init__(self, name, *, command=None, commands=None, default=None, doc='', index=0): if default is None: default = [] super().__init__(name, command=command, commands=commands, default=default, doc=doc, index=index) @property def filename(self): """ The list of full filenames represented by the value, after concatenation with the value of "boot.prefix". """ prefix = self._query('boot.prefix').value return [prefix + item for item in self.value] @property def hint(self): if self.value and self._query('boot.prefix').modified: return _('{!r} with boot.prefix').format(self.filename) else: return None def extract(self, config): for item in config: if isinstance(item, BootCommand): if item.command == 'ramfsfile': yield item, to_list(item.params, sep=',') elif item.command == 'initramfs': filename, address = item.params yield item, to_list(filename, sep=',') def update(self, value): return to_list(value) def validate(self): if self.modified and len(self.name) + sum( len(item) + 1 for item in self.value) > 80: raise ValueError(_('Excessively long list of initramfs files')) def output(self): if self.modified: new_value = ','.join(self.value) with self._override(new_value): if self._query(self._relative('.address')).value == 0: # The "followkernel" (automatic) addressing only works # with initramfs; with the ramfsaddr command it fails yield 'initramfs {self.value} followkernel'.format(self=self) else: yield from super().output() class CommandSerialEnabled(CommandBool): """ Handles the ``enable_uart`` setting and its platform-dependent defaults. """ @property def default(self): if get_board_type() in {'pi0w', 'pi3', 'pi3+', 'pi4'}: return not self._query('bluetooth.enabled').value else: return True class OverlaySerialUART(Setting): """ Handles deriving the default serial UART based on the enabled state of Bluetooth (if present) and/or the presence of the miniuart-bt overlay. """ @property def default(self): if get_board_type() in {'pi0w', 'pi3', 'pi3+', 'pi4'}: if self._query('bluetooth.enabled').value: return 1 else: return 0 else: return 0 @property def key(self): return ('overlays', 'miniuart-bt') @property def hint(self): if self.value == 0: return '/dev/ttyAMA0; PL011' else: return '/dev/ttyS0; mini-UART' def extract(self, config): for item in config: if isinstance(item, BootOverlay): if item.overlay in ('miniuart-bt', 'pi3-miniuart-bt'): yield item, 0 def update(self, value): return to_int(value) def validate(self): if self.value == 1 and not self._query('bluetooth.enabled').value: raise ValueError(_( 'serial.uart must be 0 when bluetooth.enabled is off')) def output(self): if self.modified: raise DelegatedOutput('bluetooth.enabled') else: return () class OverlayBluetoothEnabled(Setting): """ Represents the ``miniuart-bt`` and ``disable-bt`` overlays (via the ``bluetooth.enabled`` pseudo-command). """ @property def default(self): return get_board_type() in {'pi0w', 'pi3', 'pi3+', 'pi4'} @property def key(self): return ('overlays', 'disable-bt') def extract(self, config): for item in config: if isinstance(item, BootOverlay): if item.overlay in ('disable-bt', 'pi3-disable-bt'): yield item, False elif item.overlay in ('miniuart-bt', 'pi3-miniuart-bt'): yield item, True # XXX What happens if both overlays are specified? def update(self, value): return to_bool(value) def output(self): if self.modified or self._query('serial.uart').modified: # TODO what about pi3- prefix on systems with deprecated overlays? if not self.value: yield 'dtoverlay=disable-bt' elif self._query('serial.uart').value == 0: yield 'dtoverlay=miniuart-bt' class OverlayKMS(Setting): """ Represents the framebuffer driver as 'legacy' (when no overlays are used), 'fkms' (when the vc4-fkms-v3d overlay is loaded), or 'kms' (when the vc4-kms-v3d overlay is loaded). """ @property def default(self): return 'legacy' @property def key(self): return ('overlays', 'vc4-fkms-v3d') def extract(self, config): for item in config: if isinstance(item, BootOverlay): try: yield item, { 'vc4-fkms-v3d': 'fkms', 'vc4-kms-v3d': 'kms', }[item.overlay] except KeyError: pass def update(self, value): return to_str(value) def validate(self): if self.value not in {'legacy', 'kms', 'fkms'}: raise ValueError( _("{self.name} must be one of 'legacy', 'kms', " "'fkms'").format(self=self)) def output(self): try: yield 'dtoverlay=' + { 'fkms': 'vc4-fkms-v3d', 'kms': 'vc4-kms-v3d', }[self.value] except KeyError: pass @property def hint(self): return { 'legacy': 'no KMS', 'fkms': 'Fake KMS', 'kms': 'Full KMS', }[self.value] class OverlayDWC2(Setting): """ Represents the dwc-otg and dwc2 overlays. The former is default on all pi models except the 0 where the latter is default. """ @property def default(self): return get_board_type() in {'pi0', 'pi0w'} @property def key(self): return ('overlays', 'dwc2' if self.value else 'dwc-otg') def extract(self, config): for item in config: if isinstance(item, BootOverlay): if item.overlay == 'dwc-otg': yield item, False elif item.overlay == 'dwc2': yield item, True # XXX What happens if both overlays are specified? def update(self, value): return to_bool(value) def output(self): if self.modified: if self.value: yield 'dtoverlay=dwc2' else: yield 'dtoverlay=dwc-otg' class CommandCPUL2Cache(CommandBoolInv): """ Handles the ``disable_l2cache`` command. """ @property def default(self): return { 'pi0': True, 'pi0w': True, 'pi1': True, 'pi2': False, 'pi3': False, 'pi3+': False, 'pi4': False, }.get(get_board_type()) class CommandCPUFreqMax(CommandInt): """ Handles the ``arm_freq`` command. """ @property def default(self): return { 'pi0': 1000, 'pi0w': 1000, 'pi1': 700, 'pi2': 900, 'pi3': 1200, 'pi3+': 1400, 'pi4': 1500, }.get(get_board_type(), 0) def validate(self): other = self._query(self._relative('.min')) if self.value < other.value: raise ValueError(_( '{self.name} cannot be less then {other.name}').format( self=self, other=other)) @property def hint(self): return 'MHz' class CommandCPUFreqMin(CommandInt): """ Handles the ``arm_freq_min`` command. """ @property def default(self): if self._query('cpu.turbo.force').value: return self._query(self._relative('.max')).value else: return { 'pi0': 700, 'pi0w': 700, 'pi1': 700, 'pi2': 600, 'pi3': 600, 'pi3+': 600, 'pi4': 600, }.get(get_board_type(), 0) @property def hint(self): return 'MHz' class CommandCoreFreqMax(CommandInt): """ Handles the ``core_freq`` command. """ @property def default(self): if ( self._query('serial.enabled').value and self._query('serial.uart').value == 1 and not self._query('cpu.turbo.force').value): return self._query(self._relative('.min')).value else: board_type = get_board_type() if board_type == 'pi4': return ( 360 if self._query('video.tv.enabled').value else 550 if self._query('video.hdmi.4kp60').value else 500) else: return { 'pi0': 400, 'pi0w': 400, 'pi1': 250, 'pi2': 250, 'pi3': 400, 'pi3+': 400, }.get(board_type, 0) def output(self): blocks = [self] + [ self._query(self._relative( '...{block}.frequency.max'.format(block=block) )) for block in ('h264', 'isp', 'v3d') ] if any(block.modified for block in blocks): if all(self.value == block.value for block in blocks): yield 'gpu_freq={value}'.format(value=self.value) else: yield from super().output() def validate(self): other = self._query(self._relative('.min')) if self.value < other.value: raise ValueError(_( '{self.name} cannot be less then {other.name}').format( self=self, other=other)) @property def hint(self): return 'MHz' class CommandCoreFreqMin(CommandInt): """ Handles the ``core_freq_min`` command. """ @property def default(self): if self._query('cpu.turbo.force').value: return self._query(self._relative('.max')).value else: board_type = get_board_type() if board_type == 'pi4' and self._query('video.hdmi.4kp60').value: return 275 elif board_type: return 250 else: return 0 def output(self): blocks = [self] + [ self._query(self._relative( '...{block}.frequency.min'.format(block=block) )) for block in ('h264', 'isp', 'v3d') ] if any(block.modified for block in blocks): if all(self.value == block.value for block in blocks): yield 'gpu_freq_min={value}'.format(value=self.value) else: yield from super().output() @property def hint(self): return 'MHz' class CommandGPUFreqMax(CommandInt): """ Handles the ``h264_freq``, ``isp_freq``, and ``v3d_freq`` commands. """ @property def default(self): board_type = get_board_type() if board_type == 'pi4': return ( 360 if self._query('video.tv.enabled').value else 550 if self._query('video.hdmi.4kp60').value else 500) else: return { 'pi0': 300, 'pi0w': 300, 'pi1': 250, 'pi2': 250, 'pi3': 400, 'pi3+': 400, }.get(board_type, 0) def output(self): blocks = [ self._query(self._relative( '...{block}.frequency.max'.format(block=block) )) for block in ('core', 'h264', 'isp', 'v3d') ] if any(block.modified for block in blocks): if all(self.value == block.value for block in blocks): raise DelegatedOutput(self._relative('...core.frequency.max')) else: yield from super().output() def validate(self): other = self._query(self._relative('.min')) if self.value < other.value: raise ValueError(_( '{self.name} cannot be less then {other.name}').format( self=self, other=other)) @property def hint(self): return 'MHz' class CommandGPUFreqMin(CommandInt): """ Handles the ``h264_freq_min``, ``isp_freq_min``, and ``v3d_freq_min`` commands. """ @property def default(self): if self._query('cpu.turbo.force').value: return self._query(self._relative('.max')).value else: board_type = get_board_type() return 500 if board_type == 'pi4' else 250 if board_type else 0 def output(self): blocks = [ self._query(self._relative( '...{block}.frequency.min'.format(block=block) )) for block in ('core', 'h264', 'isp', 'v3d') ] if any(block.modified for block in blocks): if all(self.value == block.value for block in blocks): raise DelegatedOutput(self._relative('...core.frequency.min')) else: yield from super().output() @property def hint(self): return 'MHz' class CommandTotalMem(CommandInt): """ Handles the ``total_mem`` command. """ @property def default(self): return get_board_mem() or 1024 def extract(self, config): for item, value in super().extract(config): if item.filename == 'config.txt': # NOTE: total_mem is only valid in config.txt yield item, value def validate(self): if self.value < 128: raise ValueError(_( '{self.name} must be at least 128Mb').format(self=self)) @property def hint(self): return 'Mb' class CommandGPUMem(CommandInt): """ Handles the ``gpu_mem`` command. """ @property def default(self): return 64 if get_board_mem() < 1024 else 76 def extract(self, config): values = {name: None for name in self.commands} override = 'gpu_mem_{mem}'.format(mem=min(1024, get_board_mem())) if override not in values: override = None for item in config: # NOTE: gpu_mem_XXX is only valid in config.txt if isinstance(item, BootCommand) and item.filename == 'config.txt': # The following convoluted logic deals with the fact that # gpu_mem_1024 et al. override gpu_mem regardless of ordering if item.command in values: try: values[item.command] = to_int(item.params) except ValueError: warnings.warn(ParseWarning( '{item.filename} line {item.linenum}: invalid ' 'integer {item.params!r}'.format(item=item))) values[item.command] = None if item.command in ('gpu_mem', override): yield item, ( values['gpu_mem'] if values.get(override) is None else values.get(override) ) def validate(self): if self.value < 16: raise ValueError(_( '{self.name} must be at least 16Mb').format(self=self)) mem = get_board_mem() max_gpu_mem = { 256: 128, 512: 384, }.get(mem, 512) if self.value > max_gpu_mem: raise ValueError(_( '{self.name} must be less than {max_gpu_mem}Mb').format( self=self, max_gpu_mem=max_gpu_mem)) @property def hint(self): return 'Mb' class CommandTVOut(CommandBool): """ Handles the ``enable_tvout`` Pi4 command. """ @property def default(self): return get_board_type() != 'pi4' def validate(self): other = self._query('video.hdmi.4kp60') if self.value and other.value: raise ValueError(_( '{self.name} and {other.name} cannot both be on').format( self=self, other=other)) class CommandVideoLicense(Command): """ Handles the ``decode_MPG2`` and ``decode_WVC1`` commands. """ def __init__(self, name, *, command=None, commands=None, doc=''): super().__init__(name, command=command, commands=commands, default=[], doc=doc, index=0) def extract(self, config): for item, value in super().extract(config): yield item, to_list(value, sep=',') def update(self, value): return to_list(value) def validate(self): if self.modified and len(self.value) > 8: raise ValueError(_('Maximum of 8 licenses may be specified')) def output(self): if self.modified: new_value = ','.join(self.value) with self._override(new_value): yield from super().output() # Notes on parsing the values of the "gpio" command (from experimentation # with various pathological settings): # # 1. Any invalid values/chars on the right of the equals sign invalidates # the entire setting, e.g. "18=xx,op,dh" is entirely ignored # 2. Invalid values/chars to the left of the equals sign invalidate all # GPIO numbers after that point, but permit setting all GPIOs mentioned # until that point, e.g. "18,xx,23=op,dh" still sets GPIO18 to out/high # 3. Invalid chars include spaces, e.g. "18,23=op, dh" is entirely # ignored (by rule 1) # 4. Invalid chars in a range invalidate the entire range, e.g. # "18- 23=op,dh" sets nothing # 5. Hex-specifications are not permitted, e.g. "0x17=op,dh" is ignored # 6. Multiple valid settings on the right are permitted; only the last # apply, e.g. "18=ip,op,ip,op,dl,dh" sets GPIO18 to out/high GPIO_MODES = { 'ip': 'in', 'op': 'out', 'a0': 'alt0', 'a1': 'alt1', 'a2': 'alt2', 'a3': 'alt3', 'a4': 'alt4', 'a5': 'alt5', } GPIO_IN_STATES = { 'pd': 'down', 'pu': 'up', 'np': 'none', 'pn': 'none', } GPIO_OUT_STATES = { 'dl': 'low', 'dh': 'high', } GPIO_MODES_MAP = {v: k for k, v in GPIO_MODES.items()} GPIO_STATES_MAP = { v: k for k, v in chain( GPIO_IN_STATES.items(), GPIO_OUT_STATES.items(), ) } GPIO_STATES_MAP['none'] = 'np' # force this for consistency GPIO_COMMANDS = ( GPIO_MODES.keys() | GPIO_IN_STATES.keys() | GPIO_OUT_STATES.keys() ) def parse_gpio(s): if '=' not in s: raise ValueError('missing = in gpio specification') left, right = s.split('=', 1) commands = right.split(',') # Note we do not strip any values here (remember spaces invalidate) if set(commands) - GPIO_COMMANDS: raise ValueError('invalid command in gpio specification') mode = 'ip' state = 'np' for command in commands: if command in GPIO_MODES: mode = command else: state = command if mode == 'op' and state in GPIO_IN_STATES: state = 'dl' elif mode == 'ip' and state in GPIO_OUT_STATES: state = 'np' gpios = set() for maybe_range in left.split(','): if '-' in maybe_range: gpio_start, gpio_end = maybe_range.split('-', 1) # int() implicitly strips the input; we need to avoid that and note # an invalid point if gpio_start != gpio_start.strip() or gpio_end != gpio_end.strip(): break try: gpio_start = int(gpio_start) gpio_end = int(gpio_end) except ValueError: break else: if gpio_end < 0: break for gpio in range(gpio_start, gpio_end + 1): gpios.add(gpio) else: gpio = maybe_range if gpio != gpio.lstrip(): break try: gpio = int(gpio) except ValueError: break else: gpios.add(gpio) return ( gpios, GPIO_MODES[mode], GPIO_IN_STATES[state] if mode == 'ip' else GPIO_OUT_STATES[state] if mode == 'op' else 'none' ) class CommandGPIOMode(CommandStr): """ Handles the mode selection part of the ``gpio`` command. """ def __init__(self, name, *, command=None, commands=None, doc='', index=0): super().__init__(name, command=command, commands=commands, default='in', doc=doc, index=index, valid={ 'in': 'Input', 'out': 'Output', 'alt0': 'Alt. Function 0', 'alt1': 'Alt. Function 1', 'alt2': 'Alt. Function 2', 'alt3': 'Alt. Function 3', 'alt4': 'Alt. Function 4', 'alt5': 'Alt. Function 5', }) def extract(self, config): for item in config: if isinstance(item, BootCommand) and item.command == 'gpio': try: gpios, mode, state = parse_gpio(item.params) except ValueError: warnings.warn(ParseWarning( '{item.filename} line {item.linenum}: invalid gpio ' 'spec {item.params!r}'.format(item=item))) # TODO We've no idea if the line *would've* affected this # gpio here; probably ought to fix that else: if self.index in gpios: yield item, mode def output(self): # Only gpio0 gets to write output, and does so on behalf of all GPIO # settings if self.index > 0: if self.modified: raise DelegatedOutput('gpio0.mode') else: gpios = { gpio: ( self._query('gpio{}.mode'.format(gpio)), self._query('gpio{}.state'.format(gpio)), ) for gpio in range(28) } if any( mode.modified or state.modified for mode, state in gpios.values() ): states = { gpio: (mode.value, state.value) for gpio, (mode, state) in gpios.items() if mode.modified or state.modified } states = sorted(states.items(), key=itemgetter(1)) states = { state: set(gpio for gpio, _state in gpios) for state, gpios in groupby(states, key=itemgetter(1)) } for (mode, state), gpios in states.items(): yield 'gpio={gpios}={mode},{state}'.format( gpios=int_ranges(gpios, list_sep=','), mode=GPIO_MODES_MAP[mode], state=GPIO_STATES_MAP[state]) class CommandGPIOState(CommandStr): """ Handles the state selection part of the ``gpio`` command. """ def __init__(self, name, *, command=None, commands=None, doc='', index=0): super().__init__(name, command=command, commands=commands, default='none', doc=doc, index=index, valid={ 'up': 'Pulled up', 'down': 'Pulled down', 'none': 'No pull/floating', 'low': 'Driven low', 'high': 'Driven high', }) def extract(self, config): for item in config: if isinstance(item, BootCommand) and item.command == 'gpio': try: gpios, mode, state = parse_gpio(item.params) except ValueError: warnings.warn(ParseWarning( '{item.filename} line {item.linenum}: invalid gpio ' 'spec {item.params!r}'.format(item=item))) # TODO We've no idea if the line *would've* affected this # gpio here; probably ought to fix that else: if self.index in gpios: yield item, state def output(self): if self.modified: raise DelegatedOutput('gpio0.mode') else: return () pibootctl-0.5.2/pibootctl/settings.py000066400000000000000000002002611372751746400177470ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2019, 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . """ The :mod:`pibootctl.settings` module defines the template of all settings stored by the :class:`pibootctl.store.Settings` class. Users of the API never have any need for this module, but developers wishing to extend the set of settings will need to modify the :data:`SETTINGS` set. .. data:: SETTINGS A :class:`dict` mapping setting names to :class:`pibootctl.setting.Setting` instances which represents the complete set of settings that the application handles. """ import gettext from . import setting _ = gettext.gettext SETTINGS = { setting.OverlayParamBool( 'i2c.enabled', param='i2c_arm', doc=_( """ Enables the ARM I2C bus on pins 3 (GPIO2) and 5 (GPIO3) of the GPIO header (SDA on GPIO2, and SCL on GPIO3). """)), setting.OverlayParamInt( 'i2c.baud', param='i2c_arm_baudrate', default=100000, doc=_( """ The baud-rate of the ARM I2C bus. """)), setting.OverlayParamBool( 'spi.enabled', param='spi', doc=_( """ Enables the SPI bus on pins 19 (GPIO10), 21 (GPIO9), 23 (GPIO11), 24 (GPIO8), and 25 (GPIO7) of the GPIO header (MOSI on GPIO10, MISO on GPIO9, SCLK on GPIO11, CE0 on GPIO8, and CE1 on GPIO7). """)), setting.OverlayParamBool( 'i2s.enabled', param='i2s', doc=_( """ Enables the I2S audio bus on pins 12 (GPIO18), 35 (GPIO19), 38 (GPIO20), and 40 (GPIO21) on the GPIO header (CLK on GPIO18, FS on GPIO19, DIN on GPIO20, and DOUT on GPIO21). """)), setting.OverlayParamBool( 'audio.enabled', param='audio', doc=_( """ Enables the ALSA audio interface. """)), setting.CommandForceIgnore( 'audio.dither', force='enable_audio_dither', ignore='disable_audio_dither', doc=_( """ By default, a 1.0LSB dither is applied to the audio stream if it is routed to the analogue audio output. This can create audible background "hiss" in some situations, for example when the ALSA volume is set to a low level. Audio dither is normally disabled when audio samples are larger than 16-bits. Set this option to either force the use of dithering for all bit depths (on), or disable dithering entirely (off). """)), setting.CommandInt( 'audio.depth', command='pwm_sample_bits', default=11, doc=_( """ Adjusts the bit depth of the analogue audio output. The default bit depth is 11. Selecting bit depths below 8 will result in nonfunctional audio, as settings below 8 result in a PLL frequency too low to support. This is generally only useful as a demonstration of how bit depth affects quantisation noise. """)), setting.OverlayParamBool( 'watchdog.enabled', param='watchdog', doc=_( """ Enables the hardware watchdog. """)), setting.CommandBool( 'hat.enabled', command='force_eeprom_read', default=True, doc=_( """ Switch this option off to prevent the firmware from trying to read an I2C HAT EEPROM (connected to pins GPIO0 and GPIO1) at powerup. """)), setting.CommandBoolInv( 'video.cec.enabled', command='hdmi_ignore_cec', default=True, doc=_( """ Enables CEC (control signals) over the HDMI interface, if supported by the connected display. Switch off to pretend CEC is not supported at all. """)), setting.CommandBoolInv( 'video.cec.init', command='hdmi_ignore_cec_init', default=True, doc=_( """ When off, prevents the initial "active source" message being sent during bootup. This prevents CEC-enabled displays from coming out of standby and/or channel-switching when starting the Pi. """)), setting.Command( 'video.cec.name', command='cec_osd_name', default='Raspberry Pi', doc=_( """ The name the Pi (as a CEC device) should provide to the connected display; defaults to "Raspberry Pi". """)), setting.CommandVideoLicense( 'video.license.mpg2', command='decode_MPG2', doc=_( """ On Pi 3 and earlier models, hardware decoding of MPEG-2 can be enabled by purchasing [1] a license key which is locked to the serial number of a Raspberry Pi. Multiple license keys (up to 8) can be specified to permit switching the SD card between Pis. On the Raspberry Pi 4, the hardware codecs for MPEG-2 are permanently disabled and cannot be enabled even with a licence key; on the Pi 4, thanks to its increased processing power compared to earlier models, MPEG-2 and VC-1 can be decoded in software via applications such as VLC. Therefore, a hardware codec licence key is not needed if you're using a Pi 4. [1]: http://swag.raspberrypi.org/collections/software """)), setting.CommandVideoLicense( 'video.license.vc1', command='decode_WVC1', doc=_( """ On Pi 3 and earlier models, hardware decoding of VC-1 can be enabled by purchasing [1] a license key which is locked to the serial number of a Raspberry Pi. Multiple license keys (up to 8) can be specified to permit switching the SD card between Pis. On the Raspberry Pi 4, the hardware codecs for VC-1 are permanently disabled and cannot be enabled even with a licence key; on the Pi 4, thanks to its increased processing power compared to earlier models, MPEG-2 and VC-1 can be decoded in software via applications such as VLC. Therefore, a hardware codec licence key is not needed if you're using a Pi 4. [1]: http://swag.raspberrypi.org/collections/software """)), setting.CommandBool( 'video.hdmi.safe', command='hdmi_safe', default=False, doc=_( """ Switch on to attempt "safe mode" settings for maximum HDMI compatibility. This is the same as setting: * video.hdmi.enabled = on * video.hdmi.edid.ignore = on * video.hdmi.boost = 4 * video.hdmi.group = 2 * video.hdmi.mode = 4 * video.overscan.enabled = on * video.overscan.left = 24 * video.overscan.right = 24 * video.overscan.top = 24 * video.overscan.bottom = 24 """)), setting.CommandBool( 'video.hdmi.4kp60', command='hdmi_enable_4kp60', doc=_( """ By default, when connected to a 4K monitor, the Raspberry Pi 4B will select a 30hz refresh rate. This setting allows selection of 60Hz refresh rates. Note: enabling this will increase power consumption and increase the running temperature of the Pi. It is not possible to use 60Hz rates on both micro-HDMI ports simultaneously. Nor is it possible to enable the TV-out at the same time. """)), setting.CommandEDIDIgnore( 'video.hdmi.edid.ignore', command='hdmi_ignore_edid', doc=_( """ When on, ignores the display's EDID [1] data; useful when your display does not have an accurate EDID. [1]: https://en.wikipedia.org/wiki/Extended_Display_Identification_Data """)), setting.CommandBool( 'video.hdmi.edid.override', command='hdmi_edid_file', doc=_( """ When on, read EDID [1] data from an 'edid.dat' file, located in the boot partition, instead of reading it from the monitor. To generate an 'edid.dat' file use: $ sudo tvservice -d edid.dat [1]: https://en.wikipedia.org/wiki/Extended_Display_Identification_Data """)), setting.CommandBoolInv( 'video.hdmi.edid.parse', command='disable_fw_kms_setup', default=True, doc=_( """ By default, the firmware parses the EDID of any HDMI attached display, picks an appropriate video mode, then passes the resolution and frame rate of the mode, along with overscan parameters, to the Linux kernel via settings on the kernel command line. In rare circumstances, this can have the effect of choosing a mode that is not in the EDID, and may be incompatible with the device. You can disable this option to prevent passing these parameters and avoid this problem. The Linux video mode system (KMS) will then parse the EDID itself and pick an appropriate mode. """)), setting.CommandInt( 'video.hdmi.edid.contenttype', command='edid_content_type', valid={ 0: 'default', 1: 'graphics', 2: 'photo', 3: 'cinema', 4: 'game', }, doc=_( """ Forces the EDID content type to the specified value. Valid values are: {valid} """)), setting.CommandBool( 'video.hdmi.edid.3d', command='hdmi_force_edid_3d', doc=_( """ When on, pretends that all group 1 (CEA) HDMI modes support 3D even when the display's EDID [1] does not indicate this. Defaults to off. [1]: https://en.wikipedia.org/wiki/Extended_Display_Identification_Data """)), setting.CommandBool( 'video.hdmi.powersave', command='hdmi_blanking', doc=_( """ When enabled, if the operating system requests the display enter a low-power state using DPMS, the HDMI output will be blanked and switched off. When disabled (the default), the output is merely blanked. Note: this feature is known to cause issues with applications that don't use the framebuffer (e.g. omxplayer). Note: this feature has not yet been implemented on the Raspberry Pi 4. """)), setting.CommandBoolInv( 'video.overscan.enabled', command='disable_overscan', default=True, doc=_( """ When enabled (the default), if a group 1 (CEA) HDMI mode is selected (automatically or otherwise), the display output will include black borders to align the edges of the output with a typical TV display. """)), setting.CommandInt( 'video.overscan.left', command='overscan_left', doc=_( "The width of the left overscan border. Defaults to 0.")), setting.CommandInt( 'video.overscan.right', command='overscan_right', doc=_( "The width of the right overscan border. Defaults to 0.")), setting.CommandInt( 'video.overscan.top', command='overscan_top', doc=_( "The height of the top overscan border. Defaults to 0.")), setting.CommandInt( 'video.overscan.bottom', command='overscan_bottom', doc=_( "The height of the bottom overscan border. Defaults to 0.")), setting.CommandBool( 'video.overscan.scale', command='overscan_scale', doc=_( """ Switch on to force non-framebuffer layers to conform to the overscan settings. The default is off. Note: this feature is generally not recommended: it can reduce image quality because all layers on the display will be scaled by the GPU. Disabling overscan on the display itself is the recommended option to avoid images being scaled twice (by the GPU and the display). """)), setting.CommandInt( 'video.framebuffer.max', command='max_framebuffers', default=1, doc=_( """ Specifies the maximum number of framebuffers in the video firmware. If you have more than one display attached, you need to increase this setting to match the number of physical displays. When video.firmware.mode is 0 (legacy mode) you get one linux framebuffer per display; when it is 1 (FKMS) you still need to set this setting to match the number of physical displays but FKMS takes over the system and simulates a single framebuffer over those multiple displays. [1] [1]: https://www.raspberrypi.org/forums/viewtopic.php?t=245789 """)), setting.OverlayKMS( 'video.firmware.mode', doc=_( """ Specifies the means by which the Linux kernel communicates with the video firmware. By default this is 'legacy' (no kernel mode setting). When this is 'fkms' ("fake" kernel mode setting), the fkms overlay is loaded and the Linux kernel talks to the video firmware via the mailbox APIs for composition and output. When this is 'kms' (kernel mode setting), the full kms overlay is loaded and the Linux kernel drives the video hardware registers directly, bypassing the firmware. However, this means that facilities still running on the firmware (e.g. the camera) no longer operate correctly. [1] [1]: https://www.raspberrypi.org/forums/viewtopic.php?t=243564 """)), setting.CommandInt( 'video.framebuffer.depth', command='framebuffer_depth', default=16, valid={ 8: '8-bit framebuffer; default RGB palette is unreadable', 16: '16-bit framebuffer', 24: '24-bit framebuffer; may result in corrupted display', 32: '32-bit framebuffer; may require video.framebuffer.alpha ' 'to be disabled', }, doc=_( """ Specifies the number of bits-per-pixel (bpp) used by the console framebuffer. The default value is 16, but other valid values are: {valid} """)), setting.CommandBoolInv( 'video.framebuffer.alpha', command='framebuffer_ignore_alpha', default=True, doc=_( """ Specifies whether the console framebuffer has an alpha channel. It may be necessary to switch this off when video.framebuffer.depth is set to 32 bpp. """)), setting.CommandInt( 'video.framebuffer.priority', command='framebuffer_priority', default=0, valid={ 0: 'Main LCD', 1: 'Secondary LCD', 2: 'HDMI 0', 3: 'Composite/TV', 7: 'HDMI 1', }, doc=_( """ On a system with multiple displays, using the legacy (pre-KMS) graphics driver, this forces a specific internal display device to be the first Linux framebuffer (i.e. /dev/fb0). The values that can be specified are: {valid} """)), setting.CommandInt( 'video.framebuffer.width', command='framebuffer_width', default=0, doc=_( """ Specifies the width of the console framebuffer in pixels. The default is the display width minus the total horizontal overscan. """)), setting.CommandInt( 'video.framebuffer.width.max', command='max_framebuffer_width', default=0, doc=_( """ Specifies the maximum width of the console framebuffer in pixels. The default is not to limit the size of the framebuffer. """)), setting.CommandInt( 'video.framebuffer.height', command='framebuffer_height', default=0, doc=_( """ Specifies the height of the console framebuffer in pixels. The default is the display height minus the total vertical overscan. """)), setting.CommandInt( 'video.framebuffer.height.max', command='max_framebuffer_height', default=0, doc=_( """ Specifies the maximum height of the console framebuffer in pixels. The default is not to limit the size of the framebuffer. """)), setting.CommandTVOut( 'video.tv.enabled', command='enable_tvout', doc=_( """ On the Pi 4, the composite TV output is disabled by default, as driving the TV output slightly impairs the speed of other system clocks and slows down the entire computer as a result (older Pi models are unaffected). Enable this setting to enable TV output on the Pi 4; on older Pi models this setting has no effect (composite output is always on without performance degradation). Note that it is not possible to enable this and the video.hdmi.4kp60 option simultaneously. """)), setting.CommandInt( 'video.tv.mode', command='sdtv_mode', valid={ 0: 'NTSC', 1: 'NTSC (Japanese)', 2: 'PAL', 3: 'PAL (Brazilian)', 16: 'NTSC (Progressive)', 18: 'PAL (Progressive)', }, doc=_( """ Defines the TV standard used for composite TV output (the 4-pole "headphone" socket on newer models). Valid values are as follows: {valid} """)), setting.CommandInt( 'video.tv.aspect', command='sdtv_aspect', default=1, valid={ 1: '4:3', 2: '14:9', 3: '16:9', }, doc=_( """ Defines the aspect ratio for the composite TV output. Valid values are as follows: {valid} """)), setting.CommandBoolInv( 'video.tv.colorburst', command='sdtv_disable_colourburst', default=True, doc=_( """ Switch off to disable color-burst [1] on the composite TV output. The picture will be displayed in monochrome, but may appear sharper. [1]: https://en.wikipedia.org/wiki/Colorburst """)), setting.CommandBoolInv( 'video.dsi.enabled', command='ignore_lcd', default=True, doc=_( """ By default, an LCD display attached to the DSI connector is used when it is detected on the I2C bus. If this setting is disabled, this detection phase will be skipped and the LCD display will not be used. """)), setting.CommandBool( 'video.dsi.default', command='display_default_lcd', default=True, doc=_( """ If an LCD display is detected on the DSI connector, it will be used as the default display and will show the framebuffer. If this setting is disabled, then (usually) the HDMI output will be the default. The LCD can still be used by choosing its display number from supported applications, e.g. omxplayer. """)), setting.CommandInt( 'video.dsi.framerate', command='lcd_framerate', default=60, doc=_( """ Specifies the framerate of an LCD display connected to the DSI port. Defaults to 60Hz. """)), setting.CommandBoolInv( 'video.dsi.touch.enabled', command='disable_touchscreen', default=True, doc=_( """ Enables or disables the touchscreen of the official Raspberry Pi LCD display. """)), setting.CommandDisplayRotate( 'video.dsi.rotate', commands=('display_lcd_rotate', 'display_rotate', 'lcd_rotate'), doc=_( """ Controls the rotation of an LCD display connected to the DSI port. Valid values are 0 (the default), 90, 180, or 270. """)), setting.CommandDisplayFlip( 'video.dsi.flip', commands=('display_lcd_rotate', 'display_rotate', 'lcd_rotate'), doc=_( """ Controls the reflection (flipping) of an LCD display connected to the DSI port. Valid values are: {valid} """)), setting.CommandBool( 'video.dpi.enabled', command='enable_dpi_lcd', doc=_( """ Enables LCD displays attached to the DPI GPIOs. This is to allow the use of third-party LCD displays using the parallel display interface [1]. [1]: https://www.raspberrypi.org/documentation/hardware/raspberrypi/dpi/README.md """)), setting.CommandDisplayGroup( 'video.dpi.group', command='dpi_group', doc=_( """ Defines which list of modes should be consulted for DPI LCD output. The possible values are: {valid} CEA (Consumer Electronics Association) modes are typically used by TVs, hence overscan applies to these modes when enabled. DMT (Display Monitor Timings) modes are typically used by monitors, hence overscan is implicitly 0 with these modes. The video.dpi.mode setting must be set when this is non-zero. """)), setting.CommandDisplayMode( 'video.dpi.mode', command='dpi_mode', doc=_( """ Defines which mode will be used for DPI LCD output. This defaults to 0 which indicates it should be automatically determined from the EDID sent by the connected display. If video.dpi.group is set to 1 (CEA), this must be one of the following values: {valid_cea} In the table above, "wide" indicates a 16:9 wide-screen variant of a mode which usually has a 4:3 aspect ratio. "2x" and "4x" indicate a higher clock rate with pixel doubling or quadrupling respectively. The following values are valid if video.hdmi.group is set to 2 (DMT): {valid_dmt} Note that there is a pixel clock limit [1]. The highest supported mode is 1920x1200 at 60Hz which reduced blanking. [1]: https://www.raspberrypi.org/forums/viewtopic.php?f=26&t=20155&p=195443#p195443 """)), setting.CommandDisplayTimings( 'video.dpi.timings', command='dpi_timings', doc=_( """ An advanced setting that permits the raw timing values to be specified directly for DPI group 2, mode 87. Please refer to the "dpi_timings" section in [1] for full details. [1]: https://www.raspberrypi.org/documentation/configuration/config-txt/video.md """)), setting.CommandDPIOutput( 'video.dpi.format', command='dpi_output_format', default=1, mask=0xf, valid={ 1: '9-bit RGB666; unsupported', 2: '16-bit RGB565; config 1', 3: '16-bit RGB565; config 2', 4: '16-bit RGB565; config 3', 5: '18-bit RGB666; config 1', 6: '18-bit RGB666; config 2', 7: '24-bit RGB888', }, dummies={ '.rgb', '.clock', '.hsync.disabled', '.hsync.polarity', '.hsync.phase', '.vsync.disabled', '.vsync.polarity', '.vsync.phase', '.output.mode', '.output.disabled', '.output.polarity', '.output.phase', }, doc=_( """ Configures which GPIO pins will be used for DPI LCD output, and how those pins will be used. Valid values are: {valid} The various configurations are as follows (in the following table, R-7 means the 7th bit of the Red value, B-2 means the 2nd bit of the Blue value, etc.), when video.dpi.rgb is set to 'RGB' ordering: | GPIO | RGB565 (config 1) | RGB565 (config 2) | RGB565 (config 3) | RGB666 (config 1) | RGB666 (config 2) | RGB888 | | 27 | - | - | - | - | - | R-7 | | 26 | - | - | - | - | - | R-6 | | 25 | - | - | R-7 | - | R-7 | R-5 | | 24 | - | R-7 | R-6 | - | R-6 | R-4 | | 23 | - | R-6 | R-5 | - | R-5 | R-3 | | 22 | - | R-5 | R-4 | - | R-4 | R-2 | | 21 | - | R-4 | R-3 | R-7 | R-3 | R-1 | | 20 | - | R-3 | - | R-6 | R-2 | R-0 | | 19 | R-7 | - | - | R-5 | - | G-7 | | 18 | R-6 | - | - | R-4 | - | G-6 | | 17 | R-5 | G-7 | G-7 | R-3 | G-7 | G-5 | | 16 | R-4 | G-6 | G-6 | R-2 | G-6 | G-4 | | 15 | R-3 | G-5 | G-5 | G-7 | G-5 | G-3 | | 14 | G-7 | G-4 | G-4 | G-6 | G-4 | G-2 | | 13 | G-6 | G-3 | G-3 | G-5 | G-3 | G-1 | | 12 | G-5 | G-2 | G-2 | G-4 | G-2 | G-0 | | 11 | G-4 | - | - | G-3 | - | B-7 | | 10 | G-3 | - | - | G-2 | - | B-6 | | 9 | G-2 | - | B-7 | B-7 | B-7 | B-5 | | 8 | B-7 | B-7 | B-6 | B-6 | B-6 | B-4 | | 7 | B-6 | B-6 | B-5 | B-5 | B-5 | B-3 | | 6 | B-5 | B-5 | B-4 | B-4 | B-4 | B-2 | | 5 | B-4 | B-4 | B-3 | B-3 | B-3 | B-1 | | 4 | B-3 | B-3 | B-2 | B-2 | B-2 | B-0 | If video.dpi.rgb is set to an order other than 'RGB', swap the colors in the table above accordingly. The other GPIOs typically used in such displays are as follows, but please refer to your board's specific documentation as these may vary: | GPIO | Function | | 3 | H-Sync | | 2 | V-Sync | | 1 | Output Enable | | 0 | Clock | For more information on DPI configuration, please refer to [1]. [1]: https://www.raspberrypi.org/documentation/hardware/raspberrypi/dpi/README.md """)), setting.CommandDPIDummy( 'video.dpi.rgb', command='dpi_output_format', default=0, mask=0xf0, valid={ 0: 'RGB', 1: 'RGB', 2: 'BGR', 3: 'GRB', 4: 'BRG', }, doc=_( """ Configures the ordering of RGB data sent to the DPI LCD display. Valid values are: {valid} For more information on DPI configuration, please refer to [1]. [1]: https://www.raspberrypi.org/documentation/hardware/raspberrypi/dpi/README.md """)), setting.CommandDPIDummy( 'video.dpi.output.mode', command='dpi_output_format', default=False, mask=0x100, doc=_( """ When off (the default), the DPI LCD's output-enable operates in "data valid" mode. When on, it operates in "combined sync" mode. For more information on DPI configuration, please refer to [1]. [1]: https://www.raspberrypi.org/documentation/hardware/raspberrypi/dpi/README.md """)), setting.CommandDPIDummy( 'video.dpi.clock', command='dpi_output_format', default=False, mask=0x200, doc=_( """ When off (the default), the DPI LCD's RGB data changes on the rising edge, and is stable at the falling edge. Switch this on to indicate that RGB data changes on the falling edge and is stable at the rising edge. For more information on DPI configuration, please refer to [1]. [1]: https://www.raspberrypi.org/documentation/hardware/raspberrypi/dpi/README.md """)), setting.CommandDPIDummy( 'video.dpi.hsync.disabled', command='dpi_output_format', default=False, mask=0x1000, doc=_( """ Switch this on to disable the horizontal sync of the DPI LCD display. For more information on DPI configuration, please refer to [1]. [1]: https://www.raspberrypi.org/documentation/hardware/raspberrypi/dpi/README.md """)), setting.CommandDPIDummy( 'video.dpi.vsync.disabled', command='dpi_output_format', default=False, mask=0x2000, doc=_( """ Switch this on to disable the vertical sync of the DPI LCD display. For more information on DPI configuration, please refer to [1]. [1]: https://www.raspberrypi.org/documentation/hardware/raspberrypi/dpi/README.md """)), setting.CommandDPIDummy( 'video.dpi.output.disabled', command='dpi_output_format', default=False, mask=0x4000, doc=_( """ Switch this on to disable the output-enable of the DPI LCD display. For more information on DPI configuration, please refer to [1]. [1]: https://www.raspberrypi.org/documentation/hardware/raspberrypi/dpi/README.md """)), setting.CommandDPIDummy( 'video.dpi.hsync.polarity', command='dpi_output_format', default=False, mask=0x10000, doc=_( """ Switch this on to invert the polarity of the horizontal sync signal for the DPI LCD display. By default this is off, indicating the polarity of the signal is the same as that given by the HDMI mode driving the display. For more information on DPI configuration, please refer to [1]. [1]: https://www.raspberrypi.org/documentation/hardware/raspberrypi/dpi/README.md """)), setting.CommandDPIDummy( 'video.dpi.vsync.polarity', command='dpi_output_format', default=False, mask=0x20000, doc=_( """ Switch this on to invert the polarity of the vertical sync signal for the DPI LCD display. By default this is off, indicating the polarity of the signal is the same as that given by the HDMI mode driving the display. For more information on DPI configuration, please refer to [1]. [1]: https://www.raspberrypi.org/documentation/hardware/raspberrypi/dpi/README.md """)), setting.CommandDPIDummy( 'video.dpi.output.polarity', command='dpi_output_format', default=False, mask=0x40000, doc=_( """ Switch this on to invert the polarity of the output-enable signal for the DPI LCD display. By default this is off, indicating the polarity of the signal is the same as that given by the HDMI mode driving the display. For more information on DPI configuration, please refer to [1]. [1]: https://www.raspberrypi.org/documentation/hardware/raspberrypi/dpi/README.md """)), setting.CommandDPIDummy( 'video.dpi.hsync.phase', command='dpi_output_format', default=False, mask=0x100000, doc=_( """ Switch this on to invert the phase of the horizontal sync signal for the DPI LCD display. By default this is off, indicating the signal switches on the "positive" edge (where positive is dictated by the polarity of the signal). When on, the signal switches on the "negative" edge. For more information on DPI configuration, please refer to [1]. [1]: https://www.raspberrypi.org/documentation/hardware/raspberrypi/dpi/README.md """)), setting.CommandDPIDummy( 'video.dpi.vsync.phase', command='dpi_output_format', default=False, mask=0x200000, doc=_( """ Switch this on to invert the phase of the vertical sync signal for the DPI LCD display. By default this is off, indicating the signal switches on the "positive" edge (where positive is dictated by the polarity of the signal). When on, the signal switches on the "negative" edge. For more information on DPI configuration, please refer to [1]. [1]: https://www.raspberrypi.org/documentation/hardware/raspberrypi/dpi/README.md """)), setting.CommandDPIDummy( 'video.dpi.output.phase', command='dpi_output_format', default=False, mask=0x400000, doc=_( """ Switch this on to invert the phase of the output-enable signal for the DPI LCD display. By default this is off, indicating the signal switches on the "positive" edge (where positive is dictated by the polarity of the signal). When on, the signal switches on the "negative" edge. For more information on DPI configuration, please refer to [1]. [1]: https://www.raspberrypi.org/documentation/hardware/raspberrypi/dpi/README.md """)), setting.CommandBool( 'video.dispmanx.offline', command='dispmanx_offline', default=False, doc=_( """ Forces dispmanx composition to be done offline in two offscreen framebuffers. This can allow more dispmanx elements to be composited, but is slower and may limit screen framerate to typically 30fps. """)), setting.Command( 'boot.prefix', command='os_prefix', default='', doc=_( """ The string to prefix all filenames (e.g. boot.kernel.filename, boot.devicetree.filename, etc.) with. Note that this is literally a prefix, not a directory. For example, if boot.prefix is "foo-" and boot.kernel.filename is "kernel.img" then "foo-kernel.img" will be used as the kernel's filename. Hence, if you wish to specify a directory, make sure to end the value with "/". """)), setting.CommandBool( 'boot.test.enabled', command='test_mode', default=False, doc=_( """ When activated, display a test image and sound during boot (over the composite video and analog audio outputs only) for a given number of seconds, before continuing to boot the OS as normal. This is used as a manufacturing test; the default is off. """)), setting.CommandKernelAddress( 'boot.kernel.address', commands=('kernel_address', 'kernel_old'), default=None, doc=_( """ Specifies the address at which the bootloader should place the kernel (typically Linux). Defaults to 0x80000 when boot.arm.64bit is enabled, or 0x8000 otherwise. """)), setting.CommandKernel64( 'boot.kernel.64bit', commands=('arm_64bit', 'arm_control'), doc=_( """ Controls whether the bootloader assumes that a 64-bit kernel is to be loaded. Note that this setting affects the defaults for boot.kernel.filename and boot.kernel.address. """)), setting.CommandKernelFilename( 'boot.kernel.filename', command='kernel', doc=_( """ Specifies the kernel that the bootloader should load and execute. The defaults for the various Pi models are: | Models | Default | | Pi 1, Pi Zero, Compute Module | kernel.img | | Pi 2, Pi 3, Pi 3+, Compute Module 3+ | kernel7.img | | Pi 4 | kernel7l.img | However, if boot.kernel.64bit is on (only valid on the Pi 2 rev 1.2 and above), the default is 'kernel8.img'. """)), setting.CommandKernelCmdline( # TODO What about cmdline content? 'boot.kernel.cmdline', command='cmdline', default='cmdline.txt', doc=_( """ Specifies the alternative filename on the boot partition from which to read the kernel command line string. """)), setting.CommandBoolInv( 'boot.kernel.atags', command='disable_commandline_tags', default=True, doc=_( """ Switch this off to stop the bootloader from filling in ATAGS (memory from 0x100) before launching the kernel. """)), setting.CommandDeviceTreeAddress( 'boot.devicetree.address', command='device_tree_address', default=0, doc=_( """ Used to override the address where the bootloader loads the device tree (not dt-blob). By default the firmware will choose a suitable place. """)), setting.CommandInt( 'boot.devicetree.limit', command='device_tree_end', doc=_( """ Sets an (exclusive) limit to the loaded device tree. By default the device tree can grow to the end of usable memory, which is almost certainly what is required. """)), setting.CommandDeviceTree( 'boot.devicetree.filename', command='device_tree', default='', doc=_( """ Specifies the particular device tree that the bootloader should load and pass to the kernel. The default is for the bootloader to automatically select a device tree for the platform that it is running on (it is unusual to require a specific device tree). """)), setting.CommandFirmwareDebug( 'boot.debug.enabled', commands=('start_debug', 'start_file', 'fixup_file'), doc=_( """ Enables loading the debugging firmware. This implies that start_db.elf (or start4db.elf) will be loaded as the GPU firmware rather than the default start.elf (or start4.elf). Note that the debugging firmware incorporates the camera firmware so this will implicitly switch camera.enabled on if it is not already. The debugging firmware performs considerably more logging than the default firmware but at a performance cost, ergo it should only be used when required. """)), # TODO uart_2ndstage is only valid in config.txt setting.CommandBool( 'boot.debug.serial', command='uart_2ndstage', doc=_( """ Setting boot.debug.serial to on causes the second-stage loader (bootcode.bin on devices prior to the Raspberry Pi 4, or the boot code in the EEPROM for Raspberry Pi 4 devices) and the main firmware (start*.elf) to output diagnostic information to UART0. Be aware that output is likely to interfere with Bluetooth operation unless it is disabled (bluetooth.enabled is off) or switched to the other UART (serial.uart is 0), and if the UART is accessed simultaneously to output from Linux then data loss can occur leading to corrupted output. This feature should only be required when trying to diagnose an early boot loading problem. """)), setting.CommandTotalMem( 'boot.mem', command='total_mem', doc=_( """ This parameter, which is primarily intended for debugging, can be used to force a Raspberry Pi to limit its memory capacity: specify the total amount of RAM, in megabytes, you wish the Pi to use. For example, to make a 4GB Raspberry Pi 4B behave as though it were a 1GB model, use 1024. This value will be clamped between a minimum of 128MB, and a maximum of the total memory installed on the board. """)), setting.CommandFirmwareFilename( 'boot.firmware.filename', command='start_file', doc=_( """ The filename of the GPU firmware that the bootloader should load The GPU firmware is also the tertiary bootloader which is responsible for launching the kernel (specified by boot.kernel.filename). Usually there is no need to modify this setting directly; if you require the camera firmware simply set camera.enabled. However, if you require the specialized debugging (start_db.elf) or lightweight (start_cd.elf) firmwares you may need to specify them manually here. Please note that if you manually specify a GPU firmware, you should also manually specify an appropriate boot.firmware.fixup file. """)), setting.CommandFirmwareFixup( 'boot.firmware.fixup', command='fixup_file', doc=_( """ The filename of the fixup file for the GPU firmware specified in boot.firmware.filename. Usually there is no need to modify this setting directly; if you require the camera firmware simply set camera.enabled. However, if you require the specialized debugging (start_db.elf) or lightweight (start_cd.elf) firmwares you may need to specify them manually here. """)), setting.CommandRamFSAddress( 'boot.initramfs.address', commands=('ramfsaddr', 'initramfs'), doc=_( """ The address at which the bootloader should place the initramfs. By default this is 0, which causes the bootloader to place the initramfs immediately after the kernel in memory. """)), setting.CommandRamFSFilename( 'boot.initramfs.filename', commands=('ramfsfile', 'initramfs'), default='', doc=_( """ The filename of the (optional) initramfs that the bootloader should load and pass to the kernel. By default this is unset and no initramfs is loaded. Sufficiently new firmwares support the loading of multiple ramfs files; specify a list of filenames in this case. All loaded files are concatenated in memory and treated as a single ramfs blob. Note: the bootloader has a strict line-length of 80 characters. If many ramfs files are specified, it's possible to exceed this limit. """)), # TODO bootcode_delay is only valid in config.txt setting.CommandInt( 'boot.delay.1', command='bootcode_delay', default=0, doc=_( """ Specifies the number of seconds to delay during "bootcode.bin" (actually the second stage bootloader, despite the setting's name, but the first configurable by the boot configuration). This is particularly useful to insert a delay before reading the EDID of the monitor, for example if the Pi and monitor are powered from the same source, but the monitor takes longer to start up than the Pi. Try setting this value if the display detection is wrong on initial boot, but is correct if you soft-reboot the Pi without removing power from the monitor. """)), setting.CommandBootDelay2( 'boot.delay.2', commands=('boot_delay', 'boot_delay_ms'), default=0, doc=_( """ Specifies the number of seconds to delay during "start.elf" (actually the third stage bootloader prior to the kernel itself). This can be useful if your SD card needs a while to get ready before Linux is able to boot from it. """)), setting.CommandBoolInv( 'boot.splash.enabled', command='disable_splash', default=True, doc=_( """ If this is switched off, the rainbow splash screen will not be shown on boot. """)), setting.CommandSerialEnabled( 'serial.enabled', command='enable_uart', doc=_( """ Enable the primary/console UART (ttyS0 on a Pi 3, ttyAMA0 otherwise, unless swapped with an overlay such as miniuart-bt). If the primary UART is UART0 (the PL011, or ttyAMA0 in Linux) then this setting defaults to on, otherwise it defaults to off. This is because, when the primary UART is UART1 (the mini-UART, or ttyS0 in Linux), it is necessary to stop the core VPU frequency from changing which would make the UART unusable. Under these circumstances, activating serial.enabled implies gpu.core.frequency.max=250 (unless cpu.turbo.force is on). In some cases this is a performance hit, so it is off by default. More details on UARTs can be found at [1]. [1]: https://www.raspberrypi.org/documentation/configuration/uart.md """)), setting.CommandInt( 'serial.baud', command='init_uart_baud', default=115200, doc=_( """ Sets the initial baud rate for the primary UART. """)), setting.CommandInt( 'serial.clock', command='init_uart_clock', default=48000000, doc=_( """ Sets the initial UART clock frequency. Note that this clock only applies to UART0 (the PL011, or /dev/ttyAMA0 in Linux), and that the maximum baud-rate for the UART is limited to 1/16th of the clock. The default UART on the Pi 3 and Pi Zero is UART1 (the mini-UART, or ttyS0 in Linux), and its clock is the core VPU clock: at least 250MHz. """)), setting.OverlaySerialUART( 'serial.uart', doc=_( """ Controls whether the primary UART is UART1 (the mini-UART, or ttyS0 in Linux) or UART0 (the PL011, ttyAMA0 in Linux). By default, on Raspberry Pis equipped with the wireless/Bluetooth module (Raspberry Pi 3 and later, and the Raspberry Pi Zero W), UART0 is connected to the Bluetooth module, while UART1 is used as the primary UART and may have a Linux console on it. On all other models, UART0 is used as the primary UART. More details on UARTs can be found at [1]. [1]: https://www.raspberrypi.org/documentation/configuration/uart.md """)), setting.OverlayBluetoothEnabled( 'bluetooth.enabled', doc=_( """ Controls whether the Bluetooth module (Raspberry Pi 3 and later, and the Raspberry Pi Zero W), is enabled (which it is by default). Note that disabling the module can affect the default state of serial.enabled and serial.uart. """)), setting.OverlayDWC2( 'usb.dwc2.enabled', doc=_( """ Selects the USB controller driver for the DWC2 driven USB port. On the Raspberry Pi 4 this is the USB-C (power) port. On the A+ and 3A+ this is the single USB type A ("full size") port. On the Pi Zero this is the micro-USB port labelled "USB". On all other models, this USB port is not (directly) accessible as it sits behind the combined USB hub and ethernet controller. On the Raspberry Pi Zero, this setting defaults to enabled meaning the "dwc2" driver is selected permitting dual-role (gadget) operation (depending on the setting of usb.dwc2.mode). On all other models, this setting defaults to disabled which results in the "dwc-otg" driver (which supports fast interrupts) being used. """)), setting.OverlayParamStr( 'usb.dwc2.mode', overlay='dwc2', param='dr_mode', default='otg', valid={ 'host': 'Host mode always', 'peripheral': 'Device mode always', 'otg': 'Dual-role host/device', }, doc=_( """ Selects the dual-role mode of the DWC2 USB port, when usb.dwc2.enabled is true. This can be one of: {valid} """)), setting.CommandFirmwareCamera( 'camera.enabled', commands=('start_x', 'start_debug', 'start_file', 'fixup_file'), doc=_( """ Enables loading the Pi camera module firmware. This implies that start_x.elf (or start4x.elf) will be loaded as the GPU firmware rather than the default start.elf (and the corresponding fixup file). Note: with the camera firmware loaded, gpu.mem must be 64Mb or larger (128Mb is recommended for most purposes; 256Mb may be required for complex processing pipelines). """)), setting.CommandBoolInv( 'camera.led.enabled', command='disable_camera_led', default=True, doc=_( """ Switch this off to disable the red power LED on the Pi Camera Module version 1. This is useful for preventing reflections when the camera is facing a window, for example. """)), setting.CommandCPUL2Cache( 'cpu.l2.enabled', command='disable_l2cache', doc=_( """ Switching this off disables the CPU's access to the GPU's L2 cache and requires a corresponding L2 disabled kernel. Default value on the Pi Zero and Pi 1 is on. On all other models (currently Pi 2, Pi 3, Pi 3+, and Pi 4), the ARMs have their own L2 cache and therefore the default is off. The standard Pi kernel.img and kernel7.img builds reflect this difference in cache setting. """)), setting.CommandBool( 'cpu.gic.enabled', command='enable_gic', default=True, doc=_( """ On the Raspberry Pi 4B, if this setting is switched off then interrupts will be routed to the ARM cores using the legacy interrupt controller, rather than via the GIC-400. """)), setting.CommandBool( 'cpu.turbo.force', command='force_turbo', default=False, doc=_( """ Forces turbo mode frequencies even when the ARM cores are not busy. Enabling this may set the warranty bit if certain overvolt.* settings are also set. """)), setting.CommandInt( 'cpu.turbo.initial', command='initial_turbo', default=0, doc=_( """ Enables turbo mode from boot for the given number of seconds, or until cpufreq sets a frequency. For more information see [1]. The maximum value is 60 seconds. [1]: https://www.raspberrypi.org/forums/viewtopic.php?f=29&t=6201&start=425#p180099 """)), setting.CommandCPUFreqMax( 'cpu.frequency.max', command='arm_freq', doc=_( """ The maximum frequency of the ARM CPU in MHz. The default values for various models are as follows: | Model | Frequency (MHz) | | Pi 0 | 1000 | | Pi 1 | 700 | | Pi 2 | 900 | | Pi 3 | 1200 | | Pi 3+ | 1400 | | Pi 4 | 1500 | """)), setting.CommandCPUFreqMin( 'cpu.frequency.min', command='arm_freq_min', doc=_( """ The minimum value of cpu.frequency.max used for dynamic frequency clocking. """)), setting.CommandCoreFreqMax( 'gpu.core.frequency.max', commands=('core_freq', 'gpu_freq'), doc=_( """ Frequency of the GPU processor core in MHz. Influences CPU performance because it drives the L2 cache and memory bus. The default values for various models are as follows: | Model | Frequency (Mhz) | | Pi 0 | 400 | | Pi 1 | 250 | | Pi 2 | 250 | | Pi 3 | 400 | | Pi 3+ | 400 | | Pi 4 | 500 | 600 is the only other accepted value. The L2 cache benefits only Pi Zero / Pi Zero W / Pi 1, there is a small benefit for SDRAM on Pi 2 / Pi 3 and Pi 4B. """)), setting.CommandCoreFreqMin( 'gpu.core.frequency.min', commands=('core_freq_min', 'gpu_freq_min'), doc=_( """ Minimum value of gpu.frequency.core.max used for dynamic frequency clocking. The default value is 250Mhz. On Pi 4B the default is 275Mhz when video.hdmi.mode.4kp60 is on. """)), setting.CommandGPUFreqMax( 'gpu.h264.frequency.max', commands=('h264_freq', 'gpu_freq'), doc=_( """ Frequency of the GPU's hardware video block in MHz. The default values for various models are as follows: | Model | Frequency (Mhz) | | Pi 0 | 400 | | Pi 1 | 250 | | Pi 2 | 250 | | Pi 3 | 300 | | Pi 3+ | 300 | | Pi 4 | 500 | 600Mhz is the only other accepted value. """)), setting.CommandGPUFreqMin( 'gpu.h264.frequency.min', commands=('h264_freq_min', 'gpu_freq_min'), doc=_( """ Minimum value of gpu.frequency.h264.max used for dynamic frequency clocking. The default value is 250, or 500 on Pi 4B. """)), setting.CommandGPUFreqMax( 'gpu.isp.frequency.max', commands=('isp_freq', 'gpu_freq'), doc=_( """ Frequency of the GPU's image sensor pipeline block in MHz. The default values for various models are as follows: | Model | Frequency (Mhz) | | Pi 0 | 400 | | Pi 1 | 250 | | Pi 2 | 250 | | Pi 3 | 300 | | Pi 3+ | 300 | | Pi 4 | 500 | 600Mhz is the only other accepted value. """)), setting.CommandGPUFreqMin( 'gpu.isp.frequency.min', commands=('isp_freq_min', 'gpu_freq_min'), doc=_( """ Minimum value of gpu.frequency.isp.max used for dynamic frequency clocking. The default value is 250, or 500 on Pi 4B. """)), setting.CommandGPUFreqMax( 'gpu.v3d.frequency.max', commands=('v3d_freq', 'gpu_freq'), doc=_( """ Frequency of the GPU's 3D block in MHz. The default values for various models are as follows: | Model | Frequency (Mhz) | | Pi 0 | 400 | | Pi 1 | 250 | | Pi 2 | 250 | | Pi 3 | 300 | | Pi 3+ | 300 | | Pi 4 | 500 | 600Mhz is the only other accepted value. """)), setting.CommandGPUFreqMin( 'gpu.v3d.frequency.min', commands=('v3d_freq_min', 'gpu_freq_min'), doc=_( """ Minimum value of gpu.frequency.v3d.max used for dynamic frequency clocking. The default value is 250, or 500 on Pi 4B. """)), setting.CommandGPUMem( 'gpu.mem', commands=('gpu_mem', 'gpu_mem_256', 'gpu_mem_512', 'gpu_mem_1024'), doc=_( """ Specifies how much memory, in megabytes, to reserve for the exclusive use of the GPU: the remaining memory is allocated to the CPU. For models with less than 1GB of memory, the default is 64; for model with 1GB or more of memory the default is 76. To ensure the best performance of Linux, you should set this to the lowest possible value. If a particular graphics feature is not working correctly, try increasing the value, being mindful of the recommended maximums shown below. There is no performance advantage from specifying values larger than is necessary. The maximum values are as follows: | Total RAM | Maximum | | 256MB | 128 | | 512MB | 384 | | 1GB+ | 512 | The minimum value is 16, however this disables certain GPU features. On the Raspberry Pi 4 the 3D component of the GPU has its own memory management unit (MMU), and does not use memory from this allocation. Instead memory is allocated dynamically within Linux. This may allow a smaller value to be specified for on the Pi 4, compared to previous models. """)), } SETTINGS |= {spec for gpio in range(28) for spec in ( setting.CommandGPIOMode( 'gpio{}.mode'.format(gpio), index=gpio, command='gpio', doc=_( """ Allows GPIO pins to be set to specific modes at boot time in a way that would previously have needed a custom device-tree blob. The valid modes are: {valid} The associated gpio{index}.state can be used to set pulls (for inputs) or drives (for outputs). GPIO changes made through this mechanism do not have any direct effect on the kernel; they don't cause GPIO pins to be exported to the sysfs interface, and they can be overridden by pinctrl entries in the Device Tree as well as utilities like raspi-gpio. """)), setting.CommandGPIOState( 'gpio{}.state'.format(gpio), index=gpio, command='gpio', doc=_( """ Allows GPIO pins to be set to specific modes at boot time in a way that would previously have needed a custom device-tree blob. The valid states are: {valid} Pulls are only valid if the corresponding gpio{index}.mode is set to "in". Likewise drives are only valid when the corresponding mode is "out". GPIO changes made through this mechanism do not have any direct effect on the kernel; they don't cause GPIO pins to be exported to the sysfs interface, and they can be overridden by pinctrl entries in the Device Tree as well as utilities like raspi-gpio. """)), )} SETTINGS |= {spec for hdmi in (0, 1) for spec in ( setting.CommandForceIgnore( 'video.hdmi{}.enabled'.format(hdmi), index=hdmi, force='hdmi_force_hotplug', ignore='hdmi_ignore_hotplug', doc=_( """ Switch on to force HDMI output to be used even if no HDMI monitor is attached (forces the HDMI hotplug signal to be asserted). Switch off to force composite TV output even if an HDMI display is detected (ignores the HDMI hotplug signal). """)), setting.CommandForceIgnore( 'video.hdmi{}.audio'.format(hdmi), index=hdmi, force='hdmi_force_edid_audio', ignore='hdmi_ignore_edid_audio', doc=_( """ Switch on to force the HDMI output to assume that all audio formats are supported by the display. Switch off to assume that no audio formats are supported by the display (ignoring the EDID [1] data given by the attached display). [1]: https://en.wikipedia.org/wiki/Extended_Display_Identification_Data """)), setting.CommandIncludedFile( 'video.hdmi{}.edid.filename'.format(hdmi), index=hdmi, default='edid.dat', command='hdmi_edid_filename', doc=_( """ On the Raspberry Pi 4B, you can manually specify the file to read for alternate EDID [1] data. Note that this still requires video.hdmi.edid.override to be set. [1]: https://en.wikipedia.org/wiki/Extended_Display_Identification_Data """)), setting.CommandHDMIBoost( 'video.hdmi{}.boost'.format(hdmi), index=hdmi, command='config_hdmi_boost', default=5, doc=_( """ Configures the signal strength of the HDMI interface. Must be a value between 0 and 11. The default value is 5. Raise this value to 7 if you are seeing speckling or interference. Very long HDMI cables may need 11, but values this high should not be used unless absolutely necessary. This option is ignored on the Raspberry Pi 4. """)), setting.CommandDisplayGroup( 'video.hdmi{}.group'.format(hdmi), index=hdmi, command='hdmi_group', doc=_( """ Defines which list of modes should be consulted for the HDMI output. The possible values are: {valid} CEA (Consumer Electronics Association) modes are typically used by TVs, hence overscan applies to these modes when enabled. DMT (Display Monitor Timings) modes are typically used by monitors, hence overscan is implicitly 0 with these modes. The video.hdmi{index}.mode setting must be set when this is non-zero. """)), setting.CommandDisplayMode( 'video.hdmi{}.mode'.format(hdmi), index=hdmi, command='hdmi_mode', doc=_( """ Defines which mode will be used on the HDMI output. This defaults to 0 which indicates it should be automatically determined from the EDID sent by the connected display. If video.hdmi{index}.group is set to 1 (CEA), this must be one of the following values: {valid_cea} Pixel doubling and quadrupling indicates a higher clock rate, with each pixel repeated two or four times respectively. The following values are valid if video.hdmi.group is set to 2 (DMT): {valid_dmt} Note that there is a pixel clock limit [1]. The highest supported mode on models prior to the Raspberry Pi 4 is 1920x1200 at 60Hz with reduced blanking, whilst the Raspberry Pi 4 can support up to 4096x2160 (known as 4k) at 60Hz. Also note that if you are using both HDMI ports of the Raspberry Pi 4 for 4k output, then you are limited to 30Hz on both. [1]: https://www.raspberrypi.org/forums/viewtopic.php?f=26&t=20155&p=195443#p195443 """)), setting.CommandInt( 'video.hdmi{}.encoding'.format(hdmi), index=hdmi, command='hdmi_pixel_encoding', valid={ 0: 'auto; 1 for CEA, 2 for DMT', 1: 'RGB limited; 16-235', 2: 'RGB full; 0-255', 3: 'YCbCr limited; 16-235', 4: 'YCbCr full; 0-255', }, doc=_( """ Defines the pixel encoding mode. By default, it will use the mode requested from the EDID, so you shouldn't need to change it. Valid values are: {valid} """)), setting.CommandDisplayRotate( 'video.hdmi{}.rotate'.format(hdmi), index=hdmi, commands=('display_hdmi_rotate', 'display_rotate'), doc=_( """ Controls the rotation of the HDMI output. Valid values are 0 (the default), 90, 180, or 270. """)), setting.CommandDisplayFlip( 'video.hdmi{}.flip'.format(hdmi), index=hdmi, commands=('display_hdmi_rotate', 'display_rotate'), doc=_( """ Controls the reflection (flipping) of the HDMI output. Valid values are: {valid} """)), setting.CommandBool( 'video.hdmi{}.mode.force'.format(hdmi), index=hdmi, command='hdmi_force_mode', doc=_( """ Switching this on forces the mode specified by video.hdmi.group and video.hdmi.mode to be used even if they do not appear in the enumerated list of modes. This may help if a display seems to be ignoring these settings. """)), setting.CommandDisplayTimings( 'video.hdmi{}.timings'.format(hdmi), index=hdmi, command='hdmi_timings', doc=_( """ An advanced setting that permits the raw HDMI timing values to be specified directly for HDMI group 2, mode 87. Please refer to the "hdmi_timings" section in [1] for full details. [1]: https://www.raspberrypi.org/documentation/configuration/config-txt/video.md """)), setting.CommandInt( 'video.hdmi{}.drive'.format(hdmi), index=hdmi, command='hdmi_drive', valid={ 0: 'auto', 1: 'dvi', 2: 'hdmi', }, doc=_( """ Selects the HDMI output mode from the following values: {valid} In 'dvi' mode, audio output is disabled over HDMI. """ )), )} SETTINGS = {spec.name: spec for spec in SETTINGS} pibootctl-0.5.2/pibootctl/store.py000066400000000000000000000703131372751746400172460ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . """ The :mod:`pibootctl.store` module defines classes which control a store of Raspberry Pi boot configurations, or the active boot configuration. The main class of interest is :class:`Store`. From an instance of this, one can access derivatives of :class:`BootConfiguration` for the purposes of manipulating the store of configurations, or the active boot configuration itself. Each :class:`BootConfiguration` contains an instance of :class:`Settings` which maps setting names to :class:`~pibootctl.setting.Setting` instances. See :class:`pibootctl.main` for information on obtaining an instance of :class:`Store`. .. data:: Current The key of the active boot configuration in instances of :class:`Store`. .. data:: Default The key of the default (empty) boot configuration in instances of :class:`Store`. .. autoclass:: Store :members: .. autoclass:: BootConfiguration :members: .. autoclass:: StoredConfiguration :members: .. autoclass:: MutableConfiguration :members: .. autoclass:: Settings :members: """ import os import gettext from weakref import ref from pathlib import Path from copy import deepcopy from fnmatch import fnmatch from datetime import datetime from operator import itemgetter from collections.abc import Mapping from zipfile import ZipFile, BadZipFile, ZIP_DEFLATED from .files import AtomicReplaceFile from .parser import BootParser, BootFile, BootComment, BootConditions from .setting import CommandIncludedFile from .settings import SETTINGS from .exc import InvalidConfiguration, IneffectiveConfiguration, DelegatedOutput _ = gettext.gettext class Current: "Singleton representing the active boot configuration in :class:`Store`." def __repr__(self): return 'Current' Current = Current() class Default: "Singleton representing the default boot configuration in :class:`Store`." def __repr__(self): return 'Default' Default = Default() class Store(Mapping): """ A mapping representing all boot configurations (current, default, and stored). Acts as a mapping keyed by the name of the stored configuration, or the special values :data:`Current` for the current boot configuration, or :data:`Default` for the default (empty) configuration. The values of the mapping are derivatives of :class:`BootConfiguration` which provide the parsed :class:`Settings`, along with some other attributes. The mapping is mutable and this can be used to manipulate stored boot configurations. For instance, to store the current boot configuration under the name "foo":: >>> store = Store('/boot', 'pibootctl') >>> store["foo"] = store[Current] Setting the item with the key :data:`Current` overwrites the current boot configuration:: >>> store[Current] = store["serial"] Note that items retrieved from the store are effectively immutable; modifying them (even internally) does *not* modify the content of the store. To modify the content of the store, you must request a :meth:`~BootConfiguration.mutable` copy of a configuration, modify it, and assign it back:: >>> foo = store["foo"].mutable() >>> foo.update({"serial.enabled": True}) >>> store["serial"] = foo The same applies to the current boot configuration item:: >>> current = store[Current].mutable() >>> current.update({"camera.enabled": True, "gpu.mem": 128}) >>> store[Current] = current Items can be deleted to remove them from the store, with the obvious exception of the items with the keys :data:`Current` and :data:`Default` which cannot be removed (attempting to do so will raise a :exc:`KeyError`). Furthermore, the item with the key :data:`Default` cannot be modified either. :param str boot_path: The path on which the boot partition is mounted. :param str store_path: The path (relative to *boot_path*) under which stored configurations will be saved. :param str config_root: The filename of the "root" of the configuration, i.e. the first file read by the parser, and the file in which certain commands (e.g. start_x) *must* be placed. Currently, this should always be "config.txt", the default. :param set mutable_files: The set of filenames which :class:`MutableConfiguration` instances are permitted to change. By default this is just "config.txt". :param bool comment_lines: If :data:`True`, then :class:`MutableConfiguration` will comment out lines no longer required with a # prefix. When :data:`False` (the default), such lines will be deleted instead. When adding lines, regardless of this setting, the utility will search for, and uncomment, commented out lines which match the required output. """ def __init__(self, boot_path, store_path, config_root='config.txt', mutable_files=frozenset({'config.txt'}), comment_lines=False): self._boot_path = Path(boot_path) self._store_path = self._boot_path / store_path self._config_root = config_root self._mutable_files = frozenset(mutable_files) self._comment_lines = comment_lines def _path_of(self, name): return (self._store_path / name).with_suffix('.zip') def _enumerate(self): for path in self._store_path.glob('*.zip'): with ZipFile(str(path), 'r') as arc: if arc.comment.startswith(b'pibootctl:0:'): yield path.stem def __len__(self): # +2 for the current and default configs return sum(1 for i in self._enumerate()) + 2 def __iter__(self): yield Default yield Current yield from self._enumerate() def __contains__(self, key): if key in (Current, Default): # The current and boot configurations are always present (even if # config.txt doesn't exist, there's still technically a boot # configuration - just a default one) return True else: try: with ZipFile(str(self._path_of(key)), 'r') as arc: return arc.comment.startswith(b'pibootctl:0:') except (FileNotFoundError, BadZipFile): return False def __getitem__(self, key): if key is Default: return DefaultConfiguration() elif key is Current: return BootConfiguration( self._boot_path, self._config_root, self._mutable_files, self._comment_lines) elif key in self: return StoredConfiguration( self._path_of(key), self._config_root, self._mutable_files, self._comment_lines) else: raise KeyError(_( "No stored configuration named {key}").format(key=key)) def __setitem__(self, key, item): if key is Default: raise KeyError(_( "Cannot change the default configuration")) elif key is Current: def replace_file(path, file): with AtomicReplaceFile(self._boot_path / path) as temp: temp.write(file.content) os.utime(str(self._boot_path / path), ( datetime.now().timestamp(), file.timestamp.timestamp())) old_files = set(self[Current].files.keys()) for path, file in item.files.items(): if path != self._config_root: replace_file(path, file) # config.txt is deliberately dealt with last. This ensures that, # in the case of systems using os_prefix to switch boot directories # the switch is effectively atomic try: path = self._config_root file = item.files[self._config_root] except KeyError: pass else: replace_file(path, file) # Remove files that existed in the old configuration but not the # new; this is necessary to deal with the case of switching from # a config with config.txt (or other includes) to one without # (which is a valid, default configuration). Again, for systems # using os_prefix to switch boot dirs, this must occur last for path in old_files: if not path in item.files: os.unlink(str(self._boot_path / path)) elif isinstance(key, str) and key: self._store_path.mkdir(parents=True, exist_ok=True) with ZipFile(str(self._path_of(key)), 'x', compression=ZIP_DEFLATED) as arc: arc.comment = 'pibootctl:0:{hash}\n\n{warning}'.format( hash=item.hash, warning=_( 'Do not edit the content of this archive; the line ' 'above is a hash of the content which will not match ' 'after manual editing. Please use the pibootctl tool ' 'to manipulate stored boot configurations'), ).encode('ascii') for file in item.files.values(): file.add_to_zip(arc) else: raise KeyError(_( '{key!r} is an invalid stored configuration').format(key=key)) def __delitem__(self, key): if key is Default: raise KeyError(_("Cannot remove the default configuration")) elif key is Current: raise KeyError(_("Cannot remove the current boot configuration")) else: try: self._path_of(key).unlink() except FileNotFoundError: raise KeyError(_( "No stored configuration named {key}").format(key=key)) @property def active(self): """ Returns the key of the active configuration, if any. If no configuration is currently active, returns :data:`None`. """ current = self[Current] for key in self: if key not in (Current, Default): stored = self[key] if stored.hash == current.hash: return key return None class DefaultConfiguration: """ Represents the default boot configuration with an entirely empty file-set and a fresh :class:`Settings` instance. """ @property def files(self): """ A mapping of filenames to :class:`~pibootctl.parser.BootFile` instances representing all the files that make up the boot configuration. """ return {} @property def hash(self): """ The `SHA-1`_ hash that identifies the boot configuration. This is obtained by hashing the files of the boot configuration in parsing order. .. _SHA-1: https://en.wikipedia.org/wiki/SHA-1 """ return 'da39a3ee5e6b4b0d3255bfef95601890afd80709' # empty sha1 @property def timestamp(self): """ The last modified timestamp of the boot configuration, as a :class:`~datetime.datetime`. """ return datetime(1970, 1, 1) # UNIX epoch @property def settings(self): """ A :class:`Settings` instance containing all the settings extracted from the boot configuration. """ return Settings() class BootConfiguration: """ Represents a boot configuration, as parsed from *config_root* (default "config.txt") on the boot partition (presumably mounted at *path*, a :class:`~pathlib.Path` instance). """ def __init__(self, path, config_root='config.txt', mutable_files=frozenset({'config.txt'}), comment_lines=False): self._path = path self._config_root = config_root self._mutable_files = mutable_files self._comment_lines = comment_lines self._settings = None self._files = None self._hash = None self._timestamp = None def _parse(self): parser = BootParser(self._path) parser.parse(self._config_root) self._settings = Settings() for setting in self._settings.values(): lines = [] for item, value in setting.extract(parser.config): if item.conditions.enabled: setting._value = value lines.append(item) setting._lines = tuple(lines[::-1]) for setting in self._settings.values(): if isinstance(setting, CommandIncludedFile): parser.add(setting.filename) self._files = parser.files self._hash = parser.hash self._timestamp = parser.timestamp return parser @property def path(self): """ The path (or archive or entity) containing all the files that make up the boot configuration. """ return self._path @property def config_root(self): """ The root file of the boot configuration. This is currently always "config.txt". """ return self._config_root @property def timestamp(self): """ The last modified timestamp of the boot configuration, as a :class:`~datetime.datetime`. """ if self._timestamp is None: self._parse() return self._timestamp @property def hash(self): """ The SHA1 hash that identifies the boot configuration. This is obtained by hashing the files of the boot configuration in parsing order. """ if self._hash is None: self._parse() return self._hash @property def settings(self): """ A :class:`Settings` instance containing all the settings extracted from the boot configuration. """ if self._settings is None: self._parse() return self._settings @property def files(self): """ A mapping of filenames to :class:`~pibootctl.parser.BootFile` instances representing all the files that make up the boot configuration. """ if self._files is None: self._parse() return self._files def mutable(self): """ Return a :class:`MutableConfiguration` based on the parsed content of this configuration. Note that mutable configurations are not backed by any files on disk, so nothing is actually re-written until the updated mutable configuration is assigned back to something in the :class:`Store`. """ return MutableConfiguration(self.files.copy(), self._config_root, self._mutable_files, self._comment_lines) class StoredConfiguration(BootConfiguration): """ Represents a boot configuration stored in a :class:`~zipfile.ZipFile` specified by *path*. The starting file of the configuration is given by *config_root*. All other parameters are as in :class:`BootConfiguration`. """ def __init__(self, path, config_root='config.txt', mutable_files=frozenset({'config.txt'}), comment_lines=False): super().__init__( ZipFile(str(path), 'r'), config_root, mutable_files, comment_lines) # We can grab the hash and timestamp from the arc's meta-data without # any decompression work (it's all in the uncompressed footer) comment = self.path.comment if comment.startswith(b'pibootctl:0:'): i = len('pibootctl:0:') zip_hash = comment[i:40 + i].decode('ascii') if len(zip_hash) != 40: raise ValueError(_( 'Invalid stored configuration: invalid length')) if not set(zip_hash) <= set('0123456789abcdef'): raise ValueError(_( 'Invalid stored configuration: non-hex hash')) self._hash = zip_hash # A stored archive can be empty, hence default= is required self._timestamp = max( (datetime(*info.date_time) for info in self.path.infolist()), default=datetime(1970, 1, 1)) else: # TODO Should we allow "self-made" archives without a pibootctl # header comment? We can't currently reach here because the # enumerate and contains tests check for pibootctl:0: but that # could be relaxed... assert False, 'Invalid stored configuration: missing hash' class MutableConfiguration(BootConfiguration): """ Represents a changeable boot configuration. Do not construct instances of this class directly; they are typically constructed from a *base* :class:`BootConfiguration`, by calling :meth:`~BootConfiguration.mutable`. Mutable configurations can be changed with the :meth:`update` method which will also validate the new configuration, and check that the settings were not overridden by later files. No link is maintained between the original :class:`BootConfiguration` and the mutable copy. This implies that nothing is re-written on disk when the mutable configuration is updated. The resulting configuration must be assigned back to something in the :class:`Store` in order to re-write disk files. """ def update(self, values, context): """ Given a mapping of setting names to new values, updates the values of the corresponding settings in this configuration. If a value is :data:`None`, the setting is reset to its default value. """ # Generate the "desired" settings. Note that this is a "pure" copy of # the settings without any actual configuration files backing it. We'll # use this firstly to validate the new settings are coherent, and later # to determine whether the configuration we generate matches the # desired settings. updated = self.settings.copy() for name, value in values.items(): item = updated[name] item._value = item.update(value) item._lines = () errors = {} for item in updated.values(): try: item.validate() except ValueError as exc: errors[item.name] = exc if errors: raise InvalidConfiguration(errors) # Generate a clean configuration devoid of all the lines that affected # "values", then build a final configuration from the desired settings # we generated above, and validate it results in the desired settings self._update_path(self._clean_config(values, context)) self._parse() self._update_path(self._final_config(updated, context)) self._parse() diff = updated.diff(self.settings) if diff: raise IneffectiveConfiguration(diff) def _parse(self): # Save the parsed lines of the boot configuration; the final phase of # the update method (_final_config) requires this information parser = super()._parse() self._config = parser.config def _update_path(self, new_path): # Update self._path from *new_path*, a dict mapping filenames to # lists of lines. for filename, lines in new_path.items(): try: old_file = self._path[filename] except KeyError: old_file = BootFile.empty( filename, encoding='ascii', errors='replace') new_content = ''.join(lines).encode( old_file.encoding, old_file.errors) self._path[filename] = BootFile( filename, datetime.now(), new_content, old_file.encoding, old_file.errors) def _clean_config(self, values, context): # Generate a "clean" configuration in which all lines which affected # (or would potentially affect, under *context*) the settings mentioned # in *values* are disabled or deleted files = { line.filename for name in values for line in self.settings[name].lines } new_path = { filename: list(self._path[filename].lines()) for filename in files } for name in values: for line in self.settings[name].lines: if ( line.filename in self._mutable_files and line.conditions <= context): new_file = new_path[line.filename] if self._comment_lines: if not new_file[line.linenum - 1].startswith('#'): new_file[line.linenum - 1] = ( '#' + new_file[line.linenum - 1]) else: new_file[line.linenum - 1] = '' return new_path def _final_config(self, updated, context): # Diff the new settings to figure out which settings actually need # writing, and generate content from changed settings. Here we handle # the case of settings delegating their output to other settings and # track which ones have been done to avoid duplication done = set() new_lines = {} # XXX Can new ever be None? Would that be an error? for old, new in self.settings.diff(updated): if new.name in done: continue setting = new while True: try: done.add(setting.name) new_lines[setting.key] = list(setting.output()) except DelegatedOutput as exc: setting = updated[exc.master] else: break # Search for comments that can be "uncommented" instead of writing new # lines, and otherwise record which new lines are required new_path = {} new_config = [] for key, lines in sorted(new_lines.items(), key=itemgetter(0)): for new_line in lines: for old_line in self._config: # XXX This isn't *entirely* safe when dealing with # dt-params, because anything we uncomment is potentially # out of key order in the final output if ( isinstance(old_line, BootComment) and old_line.conditions == context and old_line.comment == new_line): try: new_file = new_path[old_line.filename] except KeyError: new_file = new_path[old_line.filename] = ( list(self._path[old_line.filename].lines())) new_file[old_line.linenum - 1] = old_line.comment + '\n' break else: new_config.append(new_line) # Find the insertion-point for new_config; ideally, this is the last # line of any section in the root configuration file which matches our # desired context. Failing that, it'll be the last line of the root # configuration file insert_at = None for line in reversed(self._config): if line.filename == self.config_root: if insert_at is None: # Set a tentative insertion-point at the last line in the # root configuration file insert_at = line if line.conditions == context: # If we find a line which has conditions matching our # required context, we're done insert_at = line break if insert_at is None: # This can only happen if there's no root configuration file so # we need to generate one with the appropriate context insert_at = BootComment(self.config_root, 0, BootConditions()) # Insert the new content, prefixed with any necessary # sections to adjust the context of the insertion point (ip) if insert_at.conditions != context: # Two cases are relevant here: the above case where no root # configuration file exists, and the case where no lines in the # existing configuration match the desired context new_config.insert(0, '') new_config[1:1] = list(context.generate(insert_at.conditions)) try: new_file = new_path[self.config_root] except KeyError: try: new_file = new_path[self.config_root] = ( list(self._path[self.config_root].lines())) except KeyError: new_file = new_path[self.config_root] = [] new_config = [line + '\n' for line in new_config] new_file[insert_at.linenum:insert_at.linenum] = new_config # TODO Add an (optional?) phase to prune (/comment?) empty sections? # TODO Add an (optional?) phase to ensure [all] is always last? return new_path class Settings(Mapping): """ Represents all settings in a boot configuration; acts like an ordered mapping of names to :class:`~pibootctl.setting.Setting` objects. """ def __init__(self, items=None): if items is None: items = SETTINGS self._items = deepcopy(items) for setting in self._items.values(): setting._settings = ref(self) self._visible = set(self._items.keys()) def __len__(self): return len(self._visible) def __iter__(self): for key in self._items: if key in self._visible: yield key def __contains__(self, key): return key in self._visible def __getitem__(self, key): if key not in self._visible: raise KeyError(key) return self._items[key] def copy(self): """ Returns a distinct copy of the configuration that can be updated without affecting the original. """ new = deepcopy(self) for setting in new._items.values(): setting._settings = ref(new) return new def modified(self): """ Returns a copy of the configuration which only contains modified settings. """ # When filtering we mustn't actually remove any members of _items as # Setting instances may need to refer to a "hidden" value to, for # example, determine their default value new_visible = { name for name in self._visible if self[name].modified } copy = self.copy() copy._visible = new_visible return copy def filter(self, pattern): """ Returns a copy of the configuration which only contains settings with names matching *pattern*, which may contain regular shell globbing patterns. """ new_visible = { name for name in self._visible if fnmatch(name, pattern) } copy = self.copy() copy._visible = new_visible return copy def diff(self, other): """ Returns a set of (self, other) setting tuples for all settings that differ between *self* and *other* (another :class:`Settings` instance). If a particular setting is missing from either side, its entry will be given as :data:`None`. """ return { (setting, other[setting.name] if setting.name in other else None) for setting in self.values() if setting.name not in other or other[setting.name].value != setting.value } | { (None, other[name]) for name in other if name not in self } pibootctl-0.5.2/pibootctl/term.py000066400000000000000000000230671372751746400170650ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2019, 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . """ The :mod:`pibootctl.term` module contains various utilities for determining the type of terminal the script is running under (:func:`term_is_dumb`, :func:`term_is_utf8`, and :func:`term_size`), for directing terminal output through the system's :func:`pager`, and for constructing an overall :class:`ErrorHandler` for the script. .. autoclass:: ErrorHandler :members: .. autoclass:: ErrorAction(message, exitcode) .. autofunction:: term_is_dumb .. autofunction:: term_is_utf8 .. autofunction:: term_size .. autofunction:: pager """ import os import io import sys import fcntl import struct import locale import gettext import termios import argparse import traceback import subprocess from collections import OrderedDict, namedtuple from contextlib import contextmanager, redirect_stdout _ = gettext.gettext def term_is_dumb(): """ Returns :data:`True` if stdout is something other than a TTY (e.g. a file redirection or a pipe). """ try: stdout_fd = sys.stdout.fileno() except OSError: return True else: return not os.isatty(stdout_fd) def term_is_utf8(): "Returns :data:`True` if the code-set of the current locale is 'UTF-8'." locale.setlocale(locale.LC_ALL, '') return locale.nl_langinfo(locale.CODESET) == 'UTF-8' def term_size(): "Returns the size of the console as a (rows, cols) tuple." # POSIX query_console_size() adapted from # http://mail.python.org/pipermail/python-list/2006-February/365594.html # http://mail.python.org/pipermail/python-list/2000-May/033365.html def get_handle_size(handle): "Subroutine for querying terminal size from std handle" try: buf = fcntl.ioctl(handle, termios.TIOCGWINSZ, '12345678') row, col = struct.unpack('hhhh', buf)[0:2] return (col, row) except OSError: return None stdin, stdout, stderr = 0, 1, 2 # Try stderr first as it's the least likely to be redirected result = ( get_handle_size(stderr) or get_handle_size(stdout) or get_handle_size(stdin) ) if not result: try: fd = os.open(os.ctermid(), os.O_RDONLY) except OSError: pass else: try: result = get_handle_size(fd) finally: os.close(fd) if not result: try: result = (os.environ['COLUMNS'], os.environ['LINES']) except KeyError: # Default result = (80, 24) return result @contextmanager def pager(enable=None): """ Used as a context manager to redirect stdout to the system's pager utility ("pager", "less", or "more" are all attempted, in that order). By default (when *enable* is :data:`None`), stdout will only be redirected if stdout is connected to a TTY. If *enable* is :data:`True` stdout will always be redirected, and likewise when *enable* is :data:`False` the function will do nothing. For example, the following script should print "Hello, world!", piping the result through the system's pager:: from pibootctl.term import pager with pager(): print("Hello, world!") """ if enable is None: enable = not term_is_dumb() if enable: env = os.environ.copy() env['LESS'] = 'FRSXMK' for exe in ('pager', 'less', 'more'): try: proc = subprocess.Popen(exe, stdin=subprocess.PIPE, env=env) except FileNotFoundError: pass except OSError as exc: print(_("Failed to execute pager: {}").format(exe), file=sys.stderr) print(str(exc), file=sys.stderr) else: try: with io.TextIOWrapper(proc.stdin, encoding=sys.stdout.encoding, write_through=True) as proc_in: with redirect_stdout(proc_in): yield finally: proc.stdin.close() proc.wait() break else: yield else: yield class ErrorAction(namedtuple('ErrorAction', ('message', 'exitcode'))): """ Named tuple dictating the action to take in response to an unhandled exception of the type it is associated with in :class:`ErrorHandler`. The *message* is an iterable of lines to be output as critical error log messages, and *exitcode* is an integer to return as the exit code of the process. Either of these can also be functions which will be called with the exception info (type, value, traceback) and will be expected to return an iterable of lines (for *message*) or an integer (for *exitcode*). """ class ErrorHandler: """ Global configurable application exception handler. For "basic" errors (I/O errors, keyboard interrupt, etc.) just the error message is printed as there's generally no need to confuse the user with a complete stack trace when it's just a missing file. Other exceptions, however, are logged with the usual full stack trace. The configuration can be augmented with other exception classes that should be handled specially by treating the instance as a dictionary mapping exception classes to :class:`ErrorAction` tuples (or any 2-tuple, which will be converted to an :class:`ErrorAction`). For example:: >>> from pibootctl.term import ErrorAction, ErrorHandler >>> import sys >>> sys.excepthook = ErrorHandler() >>> sys.excepthook[KeyboardInterrupt] (None, 1) >>> sys.excepthook[SystemExit] (None, ) >>> sys.excepthook[ValueError] = (sys.excepthook.exc_message, 3) >>> sys.excepthook[Exception] = ("An error occurred", 1) >>> raise ValueError("foo is not an integer") foo is not an integer Note the lack of a traceback in the output; if the example were a script it would also have exited with return code 3. """ def __init__(self): self._config = OrderedDict([ # Exception type, (handler method, exit code) (SystemExit, (None, self.exc_value)), (KeyboardInterrupt, (None, 2)), (argparse.ArgumentError, (self.syntax_error, 2)), ]) @staticmethod def exc_message(exc_type, exc_value, exc_tb): """ Extracts the message associated with the exception (by calling :class:`str` on the exception instance). The result is returned as a one-element list containing the message. """ return [str(exc_value)] @staticmethod def exc_value(exc_type, exc_value, exc_tb): """ Returns the first argument of the exception instance. In the case of :exc:`SystemExit` this is the expected return code of the script. """ return exc_value.args[0] @staticmethod def syntax_error(exc_type, exc_value, exc_tb): """ Returns the message associated with the exception, and an additional line suggested the user try the ``--help`` option. This should be used in response to exceptions indicating the user made an error in their command line. """ return ErrorHandler.exc_message(exc_type, exc_value, exc_tb) + [ _('Try the --help option for more information.'), ] def clear(self): """ Remove all pre-defined error handlers. """ self._config.clear() def __len__(self): return len(self._config) def __contains__(self, key): return key in self._config def __getitem__(self, key): return self._config[key] def __setitem__(self, key, value): self._config[key] = ErrorAction(*value) def __delitem__(self, key): del self._config[key] def __call__(self, exc_type, exc_value, exc_tb): for exc_class, (message, value) in self._config.items(): if issubclass(exc_type, exc_class): if callable(message): message = message(exc_type, exc_value, exc_tb) if callable(value): value = value(exc_type, exc_value, exc_tb) if message is not None: for line in message: print(line, file=sys.stderr) sys.stderr.flush() raise SystemExit(value) # Otherwise, log the stack trace and the exception into the log # file for debugging purposes for line in traceback.format_exception(exc_type, exc_value, exc_tb): for msg in line.rstrip().split('\n'): print(msg, file=sys.stderr) sys.stderr.flush() raise SystemExit(1) pibootctl-0.5.2/pibootctl/userstr.py000066400000000000000000000124241372751746400176200ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . """ The :mod:`pibootctl.userstr` module provides the :class:`UserStr` class which represents unparsed user input on the command line. The module also provides a variety of functions for converting input (either from JSON, YAML, or other structured formats, or from unparsed :class:`UserStr`) into common types (:func:`to_bool`, :func:`to_int`, :func:`to_str`, etc). .. autoclass:: UserStr .. autofunction:: to_bool .. autofunction:: to_int .. autofunction:: to_float .. autofunction:: to_str .. autofunction:: to_list """ import gettext _ = gettext.gettext class UserStr(str): """ Type used to represent a value expressed as a string on the command line. In other words, any value bearing this type is a string representation of some other type (possibly :class:`str`, :class:`int`, :data:`None`, etc.) Primarily used by various conversion routines (:func:`to_bool`, :func:`to_str`, etc.) to determine whether a value is a string parsed from some serialization format (like JSON or YAML) which should be treated as a string literal. .. note:: The blank :class:`UserStr` is special in that it *always* represents :data:`None` in conversions. """ def to_bool(s): """ Converts the :class:`UserStr` (or other type) *s* to a :class:`bool`. Various "typical" string representations of true and false are accepted including "true", "yes", and "on", along with their counter-parts "false", "no", and "off". Literal :data:`None` passes through unchanged, and a blank :class:`UserStr` will convert to :data:`None`. """ if s is None: return None elif isinstance(s, UserStr): try: return { '': None, 'auto': None, 'true': True, 'yes': True, 'on': True, '1': True, 'y': True, 'false': False, 'no': False, 'off': False, '0': False, 'n': False, }[s.strip().lower()] except KeyError: raise ValueError( _('{value} is not a valid bool').format(value=s)) return bool(s) def to_int(s): """ Converts the :class:`UserStr` (or other type) *s* to a :class:`int`. As with all :class:`UserStr` conversions, blank string inputs are converted to :data:`None`, and literal :data:`None` passes through unchanged. Otherwise, decimal integers and hexi-decimal integers prefixed with "0x" are accepted. """ if s is None: return None elif isinstance(s, str): if isinstance(s, UserStr): if not s: return None s = s.strip().lower() if s[:2] == '0x': return int(s, base=16) return int(s) def to_float(s): """ Converts the :class:`UserStr` (or other type) *s* to a :class:`float`. As with all :class:`UserStr` conversions, blank string inputs are converted to :data:`None`, and literal :data:`None` passes through unchanged. Otherwise, typical floating point values (optionally prefixed with sign, optionally suffixed with an exponent) are accepted. """ if s is None: return None elif isinstance(s, str): if isinstance(s, UserStr): if not s: return None return float(s) def to_str(s): """ Converts the :class:`UserStr` (or other type) *s* to a :class:`str`. Blank :class:`UserStr` are converted to :data:`None`, and literal :data:`None` passes through unchanged. Everything else is simply passed to the :class:`str` constructor. """ if s is None: return None elif isinstance(s, UserStr): if not s: return None else: return s.strip() return str(s) def to_list(s, sep=','): """ Converts the :class:`UserStr` (or other type) *s* to a :class:`list` based on the separator character *sep* (which defaults to ","). Blank :class:`UserStr` are converted to :data:`None`, and literal :data:`None` passes through unchanged. Everything else is passed to the :class:`list` constructor. This ensures that the result is always a unique reference. """ if s is None: return None elif isinstance(s, UserStr): if not s: return None else: s = s.strip() if isinstance(s, str): if sep in s: return [elem.strip() for elem in s.split(sep)] else: return [s] return list(s) pibootctl-0.5.2/rtd_requirements.txt000066400000000000000000000000101372751746400176610ustar00rootroot00000000000000pkginfo pibootctl-0.5.2/setup.cfg000066400000000000000000000026171372751746400153640ustar00rootroot00000000000000# coding: utf-8 [metadata] name = pibootctl version = attr: pibootctl.__version__ description = Boot configuration tool for the Raspberry Pi long_description = file: README.rst author = Dave Jones author_email = dave@waveform.org.uk project_urls = Documentation = https://pibootctl.readthedocs.io/ Source Code = https://github.com/waveform80/pibootctl Issue Tracker = https://github.com/waveform80/pibootctl/issues keywords = raspberry pi boot classifiers = Development Status :: 4 - Beta Environment :: Console Intended Audience :: System Administrators License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Topic :: System :: Boot [options] packages = find: install_requires = setuptools pyyaml [options.extras_require] test = pytest pytest-cov doc = sphinx pkginfo [options.entry_points] console_scripts = pibootctl = pibootctl.main:main [tool:pytest] addopts = -rsx --cov --tb=short testpaths = tests [coverage:run] source = pibootctl branch = true [coverage:report] show_missing = true exclude_lines = raise NotImplementedError assert False pibootctl-0.5.2/setup.py000066400000000000000000000000461372751746400152470ustar00rootroot00000000000000from setuptools import setup setup() pibootctl-0.5.2/tests/000077500000000000000000000000001372751746400146775ustar00rootroot00000000000000pibootctl-0.5.2/tests/test_files.py000066400000000000000000000034111372751746400174110ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . import os from unittest import mock import pytest from pibootctl.files import * def test_atomic_write_success(tmpdir): with AtomicReplaceFile(str(tmpdir.join('foo'))) as f: f.write(b'\x00' * 4096) temp_name = f.name assert os.path.exists(str(tmpdir.join('foo'))) assert not os.path.exists(temp_name) # TODO Test file permissions? def test_atomic_write_failed(tmpdir): with pytest.raises(IOError): with AtomicReplaceFile(str(tmpdir.join('foo'))) as f: f.write(b'\x00' * 4096) temp_name = f.name raise IOError("Something went wrong") assert not os.path.exists(str(tmpdir.join('foo'))) assert not os.path.exists(temp_name) def test_umask_child_thread(): with mock.patch('threading.current_thread') as current_thread, \ mock.patch('threading.main_thread') as main_thread: current_thread.return_value = object() main_thread.return_value = object() with pytest.raises(RuntimeError): get_umask() pibootctl-0.5.2/tests/test_formatter.py000066400000000000000000000342651372751746400203250ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . from unittest import mock from collections import OrderedDict import pytest from pibootctl.formatter import * @pytest.fixture() def table_data(request): return [ ['Key', 'Value'], ['FOO', 'bar'], ['BAZ', 'A much longer value which can wrap over several lines'], ['QUUX', 'Just for completeness'], ] @pytest.fixture() def dict_data(request): return OrderedDict([ ['FOO', 'bar'], ['BAZ', 'A much longer value which can wrap over several lines'], ['QUUX', 'Just for completeness'], ]) def test_table_wrap_basic(table_data): expected = [ "Key Value ", "---- -----------------------------------------------------", "FOO bar ", "BAZ A much longer value which can wrap over several lines", "QUUX Just for completeness ", ] wrap = TableWrapper() assert wrap.wrap(table_data) == expected assert wrap.fill(table_data) == '\n'.join(expected) def test_table_wrap_no_header(table_data): expected = [ "Key Value ", "FOO bar ", "BAZ A much longer value which can wrap over several lines", "QUUX Just for completeness ", ] wrap = TableWrapper(header_rows=0) assert wrap.wrap(table_data) == expected assert wrap.fill(table_data) == '\n'.join(expected) def test_table_wrap_thin(table_data): wrap = TableWrapper(width=40) expected = [ "Key Value ", "---- -----------------------------------", "FOO bar ", "BAZ A much longer value which can wrap ", " over several lines ", "QUUX Just for completeness ", ] assert wrap.wrap(table_data) == expected assert wrap.fill(table_data) == '\n'.join(expected) def test_table_wrap_equal(): wrap = TableWrapper(width=40) table_data = [ ("aaaaa" + " aaaaa" * 4,) * 3 ] * 4 expected = [ 'aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa ', 'aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa ', 'aaaaa aaaaa aaaaa ', '------------ ------------- -------------', 'aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa ', 'aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa ', 'aaaaa aaaaa aaaaa ', 'aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa ', 'aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa ', 'aaaaa aaaaa aaaaa ', 'aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa ', 'aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa ', 'aaaaa aaaaa aaaaa ', ] assert wrap.wrap(table_data) == expected assert wrap.fill(table_data) == '\n'.join(expected) def test_table_wrap_pretty_thin(table_data): wrap = TableWrapper(width=40, **pretty_table) expected = [ "+------+-------------------------------+", "| Key | Value |", "|------+-------------------------------|", "| FOO | bar |", "| BAZ | A much longer value which can |", "| | wrap over several lines |", "| QUUX | Just for completeness |", "+------+-------------------------------+", ] assert wrap.wrap(table_data) == expected assert wrap.fill(table_data) == '\n'.join(expected) def test_table_wrap_footer(table_data): wrap = TableWrapper(width=40, footer_rows=1, **pretty_table) expected = [ "+------+-------------------------------+", "| Key | Value |", "|------+-------------------------------|", "| FOO | bar |", "| BAZ | A much longer value which can |", "| | wrap over several lines |", "|------+-------------------------------|", "| QUUX | Just for completeness |", "+------+-------------------------------+", ] assert wrap.wrap(table_data) == expected assert wrap.fill(table_data) == '\n'.join(expected) def test_table_wrap_complex(): table_data = [ ['Model', 'RAM', 'Ethernet', 'Wifi', 'Bluetooth', 'Notes'], ['Raspberry Pi 0', '512Mb', 'No', 'No', 'No', 'Lowest power draw, smallest form factor'], ['Raspberry Pi 0W', '512Mb', 'No', 'Yes', 'Yes', 'Popular in drones'], ['Raspberry Pi 3B+', '1Gb', 'Yes', 'Yes (+5GHz)', 'Yes', 'The most common Pi currently'], ['Raspberry Pi 3A+', '512Mb', 'No', 'Yes (+5GHz)', 'Yes', 'Small form factor, low power variant of the 3B+'], ] expected = [ "+---------------+-------+----------+-------------+-----------+----------------+", "| Model | RAM | Ethernet | Wifi | Bluetooth | Notes |", "|---------------+-------+----------+-------------+-----------+----------------|", "| Raspberry Pi | 512Mb | No | No | No | Lowest power |", "| 0 | | | | | draw, smallest |", "| | | | | | form factor |", "| Raspberry Pi | 512Mb | No | Yes | Yes | Popular in |", "| 0W | | | | | drones |", "| Raspberry Pi | 1Gb | Yes | Yes (+5GHz) | Yes | The most |", "| 3B+ | | | | | common Pi |", "| | | | | | currently |", "| Raspberry Pi | 512Mb | No | Yes (+5GHz) | Yes | Small form |", "| 3A+ | | | | | factor, low |", "| | | | | | power variant |", "| | | | | | of the 3B+ |", "+---------------+-------+----------+-------------+-----------+----------------+", ] wrap = TableWrapper(width=79, **pretty_table) assert wrap.wrap(table_data) == expected assert wrap.fill(table_data) == '\n'.join(expected) expected = [ "+-----------+-------+----------+-----------+-----------+------------+", "| Model | RAM | Ethernet | Wifi | Bluetooth | Notes |", "|-----------+-------+----------+-----------+-----------+------------|", "| Raspberry | 512Mb | No | No | No | Lowest |", "| Pi 0 | | | | | power |", "| | | | | | draw, |", "| | | | | | smallest |", "| | | | | | form |", "| | | | | | factor |", "| Raspberry | 512Mb | No | Yes | Yes | Popular in |", "| Pi 0W | | | | | drones |", "| Raspberry | 1Gb | Yes | Yes | Yes | The most |", "| Pi 3B+ | | | (+5GHz) | | common Pi |", "| | | | | | currently |", "| Raspberry | 512Mb | No | Yes | Yes | Small form |", "| Pi 3A+ | | | (+5GHz) | | factor, |", "| | | | | | low power |", "| | | | | | variant of |", "| | | | | | the 3B+ |", "+-----------+-------+----------+-----------+-----------+------------+", ] wrap = TableWrapper(width=69, **pretty_table) assert wrap.wrap(table_data) == expected assert wrap.fill(table_data) == '\n'.join(expected) def test_table_wrap_too_thin(table_data): expected = [ "Key Value ", "---- -----------------------------------------------------", "FOO bar ", "BAZ A much longer value which can wrap over several lines", "QUUX Just for completeness ", ] wrap = TableWrapper(width=5, **pretty_table) with pytest.raises(ValueError): wrap.wrap(table_data) def test_table_wrap_bad_init(): with pytest.raises(ValueError): TableWrapper(borders='|') with pytest.raises(ValueError): TableWrapper(corners=',-') with pytest.raises(ValueError): TableWrapper(internal_borders='foo') def test_table_wrap_align(): data = [ ('Key', 'Value'), ('foo', 1), ('bar', 2), ] expected = [ "Key Value", "--- -----", "foo 1", "bar 2", ] wrap = TableWrapper( width=40, align=lambda y, x, data: '>' if isinstance(data, int) else '<') assert wrap.wrap(data) == expected assert wrap.fill(data) == '\n'.join(expected) def test_table_wrap_format(): data = [ ('Key', 'Value'), ('foo', 1), ('bar', 2), ] expected = [ "Key Value", "--- -----", "foo 001 ", "bar 002 ", ] wrap = TableWrapper( width=40, format=lambda y, x, data: '{:03d}'.format(data) if isinstance(data, int) else str(data)) assert wrap.wrap(data) == expected assert wrap.fill(data) == '\n'.join(expected) def test_int_ranges(): assert int_ranges(set()) == '' assert int_ranges({1}) == '1' assert int_ranges({1, 2}) == '1, 2' assert int_ranges({1, 2, 3}) == '1-3' assert int_ranges({1, 2, 3, 4, 8}) == '1-4, 8' assert int_ranges({1, 2, 3, 4, 8, 9}) == '1-4, 8-9' def test_transmap(): assert ''.format_map(TransMap(foo=1)) == '' assert '{foo}{bar}'.format_map(TransMap(foo=1)) == '1{bar}' assert '{foo:02d}{bar:02d}{baz:02d}'.format_map(TransMap(foo=1, baz=3)) == '01{bar:02d}03' assert '{foo!r}{bar!s}{baz!a}'.format_map(TransMap(foo=1)) == '1{bar!s}{baz!r}' assert 'foo' in TransMap(foo=1) def test_format_dict_table(dict_data): assert '{:table}'.format(FormatDict(dict_data)) == """\ | Key | Value | | FOO | bar | | BAZ | A much longer value which can wrap over several lines | | QUUX | Just for completeness |""" def test_format_dict_list(dict_data): assert '{:list}'.format(FormatDict(dict_data)) == """\ * FOO = bar * BAZ = A much longer value which can wrap over several lines * QUUX = Just for completeness""" def test_format_dict_bad_format(dict_data): with pytest.raises(ValueError): '{:FOO}'.format(FormatDict(dict_data)) def test_render_para(): assert render("""\ This is a very long line which ought to be wrapped by the renderer. And this is another very long which also ought to get wrapped. This is a short line.""", width=40) == """\ This is a very long line which ought to be wrapped by the renderer. And this is another very long which also ought to get wrapped. This is a short line.""" def test_render_list(dict_data): assert render("{:list}".format(FormatDict(dict_data)), width=40) == """\ * FOO = bar * BAZ = A much longer value which can wrap over several lines * QUUX = Just for completeness""" assert render(""" * A list item can be defined across several lines * Or not""") == """\ * A list item can be defined across several lines * Or not""" assert render(""" * A list item can be defined across several lines * Or not""", list_space=True) == """\ * A list item can be defined across several lines * Or not""" def test_render_refs(dict_data): assert render("{:refs}".format(FormatDict(dict_data)), width=40) == """\ [FOO]: bar [BAZ]: A much longer value which can wrap over several lines [QUUX]: Just for completeness""" def test_render_table(dict_data): assert render("{:table}".format(FormatDict(dict_data)), width=40, table_style=pretty_table) == """\ +------+-------------------------------+ | Key | Value | |------+-------------------------------| | FOO | bar | | BAZ | A much longer value which can | | | wrap over several lines | | QUUX | Just for completeness | +------+-------------------------------+""" def test_render_mixed(): assert render("""\ A paragraph * Followed by a two item * list | Key | Value | | foo | 1 | | bar | 2 | * Split list * of three items * followed by | Key | Value | | foo | 1 | | bar | 2 | * A final list with a single item And a final paragraph, split over lines followed by | Key | Value | | foo | 1 | | bar | 2 | """, table_style=pretty_table) == """\ A paragraph * Followed by a two item * list +-----+-------+ | Key | Value | |-----+-------| | foo | 1 | | bar | 2 | +-----+-------+ * Split list * of three items * followed by +-----+-------+ | Key | Value | |-----+-------| | foo | 1 | | bar | 2 | +-----+-------+ * A final list with a single item And a final paragraph, split over lines followed by +-----+-------+ | Key | Value | |-----+-------| | foo | 1 | | bar | 2 | +-----+-------+""" pibootctl-0.5.2/tests/test_info.py000066400000000000000000000050601372751746400172440ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2019, 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . from unittest import mock from pibootctl.info import * def test_get_board_revision(): with mock.patch('io.open', mock.mock_open(read_data=b'\x00\xa0\x20\xd3')) as m: assert get_board_revision() == 0xa020d3 with mock.patch('io.open') as m: m.side_effect = FileNotFoundError assert get_board_revision() is None def test_get_board_serial(): with mock.patch( 'io.open', mock.mock_open(read_data=b'\x00\x00\x00\x00\x12\x34\x56\x78')) as m: assert get_board_serial() == 0x12345678 with mock.patch('io.open') as m: m.side_effect = FileNotFoundError assert get_board_serial() is None def test_get_board_types(): with mock.patch('io.open', mock.mock_open(read_data=b'\x00\xa0\x20\xd3')) as m: assert get_board_types() == {'pi3', 'pi3+'} with mock.patch('io.open', mock.mock_open(read_data=b'\x00\x00\x00\x0d')) as m: assert get_board_types() == {'pi1'} with mock.patch('io.open', mock.mock_open(read_data=b'\x00\xc0\x31\x40')) as m: assert get_board_types() == {'pi4'} with mock.patch('io.open', mock.mock_open(read_data=b'\x00\xc0\x10\xf0')) as m: assert get_board_types() == set() with mock.patch('io.open') as m: m.side_effect = FileNotFoundError assert get_board_types() == set() def test_get_board_mem(): with mock.patch('io.open', mock.mock_open(read_data=b'\x00\xa0\x20\xd3')) as m: assert get_board_mem() == 1024 with mock.patch('io.open', mock.mock_open(read_data=b'\x00\x00\x00\x0d')) as m: assert get_board_mem() == 512 with mock.patch('io.open', mock.mock_open(read_data=b'\x00\xf0\x31\x40')) as m: assert get_board_mem() == 0 with mock.patch('io.open') as m: m.side_effect = FileNotFoundError assert get_board_mem() == 0 pibootctl-0.5.2/tests/test_main.py000066400000000000000000000523631372751746400172450ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . import os import io import sys import json import yaml from datetime import datetime from unittest import mock from pathlib import Path from operator import itemgetter import pytest from pibootctl.store import Store, Current, Default from pibootctl.term import ErrorHandler from pibootctl.main import Application from pibootctl.parser import BootConditions from pibootctl.exc import IneffectiveConfiguration cond_all = BootConditions() cond_none = cond_all.evaluate('none') @pytest.fixture() def store(request, tmpdir): boot_path = Path(str(tmpdir)) store_path = boot_path / 'pibootctl' store_path.mkdir() def my_read(self, *args, **kwargs): self['defaults']['boot_path'] = str(boot_path) self['defaults']['store_path'] = str(store_path) self['defaults']['reboot_required'] = '' self['defaults']['reboot_required_pkgs'] = '' return [] with mock.patch('configparser.ConfigParser.read', my_read): yield Store(boot_path, store_path) @pytest.fixture() def main(request): return Application() def test_help(main, capsys): with pytest.raises(SystemExit) as exc_info: main(['-h']) assert exc_info.value.args[0] == 0 captured = capsys.readouterr() assert captured.out.lstrip().startswith('usage: ') # Make sure all the expected commands exist in the help text (these aren't # localizable so it's safe to test for them) assert {'status', 'get', 'set', 'load', 'save', 'diff'} <= set(captured.out.split()) with pytest.raises(SystemExit) as exc_info: main(['help']) assert exc_info.value.args[0] == 0 captured = capsys.readouterr() assert captured.out.lstrip().startswith('usage: ') assert {'status', 'get', 'set', 'load', 'save', 'diff'} <= set(captured.out.split()) def test_help_command(main, capsys): with pytest.raises(SystemExit) as exc_info: main(['help', 'status']) assert exc_info.value.args[0] == 0 captured = capsys.readouterr() assert captured.out.lstrip().startswith('usage: ') assert {'--all', '--json', '--yaml', '--shell'} <= set(captured.out.split()) def test_help_setting(main, capsys): with pytest.raises(SystemExit) as exc_info: main(['help', 'camera.enabled']) assert exc_info.value.args[0] == 0 captured = capsys.readouterr() assert captured.out.lstrip().startswith('Name: camera.enabled') assert {'start_x', 'start_debug', 'start_file', 'fixup_file'} <= set( captured.out.replace(',', '').split()) with pytest.raises(ValueError): main(['help', 'foo.bar']) def test_help_config_command(main, capsys): with pytest.raises(SystemExit) as exc_info: main(['help', 'start_x']) assert exc_info.value.args[0] == 0 captured = capsys.readouterr() assert captured.out.lstrip().startswith('Name: camera.enabled') assert {'start_x', 'start_debug', 'start_file', 'fixup_file'} <= set( captured.out.replace(',', '').split()) with pytest.raises(ValueError) as exc_info: main(['help', 'foo_bar']) assert str(exc_info.value) == 'Unknown command "foo_bar"' def test_help_config_multi(main, capsys): with pytest.raises(SystemExit) as exc_info: main(['help', 'start_file']) assert exc_info.value.args[0] == 0 captured = capsys.readouterr() assert captured.out.lstrip().startswith('start_file is affected by') def test_dump_show(main, capsys, store): current = store[Current].mutable() current.update({'video.hdmi0.group': 1, 'video.hdmi0.mode': 4}, cond_all) store[Current] = current main(['status', '--json']) captured = capsys.readouterr() assert json.loads(captured.out) == { 'video.hdmi0.group': 1, 'video.hdmi0.mode': 4} def test_dump_show_name(main, capsys, store): current = store[Current].mutable() current.update({'video.hdmi0.group': 1, 'video.hdmi0.mode': 4}, cond_all) store[Current] = current current.update({'camera.enabled': True, 'gpu.mem': 128}, cond_all) store['cam'] = current main(['show', '--json', 'cam']) captured = capsys.readouterr() assert json.loads(captured.out) == { 'video.hdmi0.group': 1, 'video.hdmi0.mode': 4, 'camera.enabled': True, 'gpu.mem': 128} def test_dump_show_filters(main, capsys, store): current = store[Current].mutable() current.update({'video.hdmi0.group': 1, 'video.hdmi0.mode': 4}, cond_all) store[Current] = current main(['status', '--json', '--all']) captured = capsys.readouterr() assert json.loads(captured.out).keys() == current.settings.keys() main(['status', '--json', '*.group']) captured = capsys.readouterr() assert json.loads(captured.out) == {'video.hdmi0.group': 1} def test_get(main, capsys, store): current = store[Current].mutable() current.update({'video.hdmi0.group': 1, 'video.hdmi0.mode': 4}, cond_all) store[Current] = current main(['get', 'video.hdmi0.group']) captured = capsys.readouterr() assert captured.out == '1\n' with pytest.raises(ValueError): main(['get', 'foo.bar']) def test_get_multi(main, capsys, store): current = store[Current].mutable() current.update({'video.hdmi0.group': 1, 'video.hdmi0.mode': 4}, cond_all) store[Current] = current main(['get', '--json', 'video.hdmi0.group', 'spi.enabled']) captured = capsys.readouterr() assert json.loads(captured.out) == {'video.hdmi0.group': 1, 'spi.enabled': False} with pytest.raises(ValueError): main(['get', '--json', 'video.hdmi0.group', 'foo.bar']) def test_set(main, store): current = store[Current].mutable() current.update({'video.hdmi0.group': 1, 'video.hdmi0.mode': 4}, cond_all) store[Current] = current changes = {'video.hdmi0.mode': 3} with mock.patch('sys.stdin', io.StringIO(json.dumps(changes))): main(['set', '--json']) current = store[Current] assert current.settings['video.hdmi0.group'].value == 1 assert current.settings['video.hdmi0.mode'].value == 3 def test_set_user(main, store): current = store[Current].mutable() current.update({'video.hdmi0.group': 1, 'video.hdmi0.mode': 4}, cond_all) store[Current] = current main(['set', 'video.hdmi0.mode=3']) current = store[Current] assert current.settings['video.hdmi0.group'].value == 1 assert current.settings['video.hdmi0.mode'].value == 3 main(['set', 'video.hdmi0.group=', 'video.hdmi0.mode=']) current = store[Current] assert not current.settings['video.hdmi0.group'].modified assert not current.settings['video.hdmi0.mode'].modified with pytest.raises(ValueError): main(['set', 'video.hdmi0.mode']) def test_save(main, store): current = store[Current].mutable() current.update({'video.hdmi0.group': 1, 'video.hdmi0.mode': 4}, cond_all) store[Current] = current assert 'foo' not in store main(['save', 'foo']) assert 'foo' in store with pytest.raises(FileExistsError): main(['save', 'foo']) main(['save', 'foo', '--force']) assert 'foo' in store def test_load(main, store): current = store[Current].mutable() current.update({'video.hdmi0.group': 1, 'video.hdmi0.mode': 4}, cond_all) store['foo'] = current assert not store[Current].settings['video.hdmi0.group'].modified with mock.patch('pibootctl.main.datetime') as dt: dt.now.return_value = datetime(2000, 1, 1) main(['load', 'foo']) assert store[Current].settings['video.hdmi0.group'].modified assert store.keys() == {Current, Default, 'foo', 'backup-20000101-000000'} def test_load_no_backup(main, store): current = store[Current].mutable() current.update({'video.hdmi0.group': 1, 'video.hdmi0.mode': 4}, cond_all) store['foo'] = current assert not store[Current].settings['video.hdmi0.group'].modified main(['load', 'foo', '--no-backup']) assert store[Current].settings['video.hdmi0.group'].modified assert set(store.keys()) == {Current, Default, 'foo'} def test_diff(main, capsys, store): current = store[Current].mutable() current.update({'video.hdmi0.group': 1, 'video.hdmi0.mode': 4}, cond_all) store['foo'] = current current.update({'video.hdmi0.mode': 5, 'spi.enabled': True}, cond_all) store['bar'] = current main(['diff', 'foo', 'bar', '--json']) captured = capsys.readouterr() assert json.loads(captured.out) == { 'video.hdmi0.mode': {'left': 4, 'right': 5}, 'spi.enabled': {'left': False, 'right': True}, } def test_list(main, capsys, store): with mock.patch('pibootctl.store.datetime') as dt: dt.now.return_value = datetime(2000, 1, 1) current = store[Current].mutable() current.update({'video.hdmi0.group': 1, 'video.hdmi0.mode': 4}, cond_all) store['foo'] = current current.update({'video.hdmi0.mode': 5, 'spi.enabled': True}, cond_all) store['bar'] = current store[Current] = store['bar'] main(['ls', '--json']) captured = capsys.readouterr() assert sorted(json.loads(captured.out), key=itemgetter('name')) == sorted([ {'name': 'foo', 'active': False, 'timestamp': '2000-01-01T00:00:00'}, {'name': 'bar', 'active': True, 'timestamp': '2000-01-01T00:00:00'}, ], key=itemgetter('name')) def test_remove(main, store): current = store[Current].mutable() current.update({'video.hdmi0.group': 1, 'video.hdmi0.mode': 4}, cond_all) store['foo'] = current assert store.keys() == {Current, Default, 'foo'} main(['rm', 'foo']) assert store.keys() == {Current, Default} with pytest.raises(FileNotFoundError): main(['rm', 'bar']) main(['rm', '-f', 'bar']) def test_rename(main, store): current = store[Current].mutable() current.update({'video.hdmi0.group': 1, 'video.hdmi0.mode': 4}, cond_all) store['foo'] = current store['bar'] = current assert store.keys() == {Current, Default, 'foo', 'bar'} main(['mv', 'foo', 'baz']) assert store.keys() == {Current, Default, 'baz', 'bar'} with pytest.raises(FileExistsError): main(['mv', 'bar', 'baz']) assert store.keys() == {Current, Default, 'baz', 'bar'} main(['mv', '-f', 'bar', 'baz']) assert store.keys() == {Current, Default, 'baz'} def test_backup_fallback(main, store): with mock.patch('pibootctl.main.datetime') as dt: dt.now.return_value = datetime(2000, 1, 1) current = store[Current].mutable() current.update({'video.hdmi0.group': 1, 'video.hdmi0.mode': 4}, cond_all) store['foo'] = current # Causes a backup to be taken with timestamp 2000-01-01 main(['load', 'foo']) assert set(store.keys()) == { Current, Default, 'foo', 'backup-20000101-000000'} # Modify the current and cause another backup to be taken without # advancing our fake timestamp current = store[Current].mutable() current.update({'video.hdmi0.group': 1, 'video.hdmi0.mode': 5}, cond_all) store[Current] = current main(['load', 'foo']) assert store.keys() == { Current, Default, 'foo', 'backup-20000101-000000', 'backup-20000101-000000-1'} def test_reboot_required(main, tmpdir): boot_path = Path(str(tmpdir)) store_path = boot_path / 'store' var_run_path = boot_path / 'run' store_path.mkdir() var_run_path.mkdir() def my_read(self, *args, **kwargs): self['defaults']['boot_path'] = str(boot_path) self['defaults']['store_path'] = str(store_path) self['defaults']['reboot_required'] = str(var_run_path / 'reboot-required') self['defaults']['reboot_required_pkgs'] = str(var_run_path / 'reboot-required.pkgs') return [] with mock.patch('configparser.ConfigParser.read', my_read): store = Store(boot_path, store_path) current = store[Current].mutable() current.update({'video.hdmi0.group': 1, 'video.hdmi0.mode': 4}, cond_all) store['foo'] = current assert not (var_run_path / 'reboot-required').exists() assert not (var_run_path / 'reboot-required.pkgs').exists() main(['load', 'foo']) assert (var_run_path / 'reboot-required').read_text() != '' assert main.config.package_name in (var_run_path / 'reboot-required.pkgs').read_text() def test_permission_error(store): with mock.patch('pibootctl.main.os.geteuid') as geteuid: geteuid.return_value = 1000 try: raise PermissionError('permission denied') except PermissionError: msg = Application.permission_error(*sys.exc_info()) assert len(msg) == 2 assert msg[0] == 'permission denied' assert 'root' in msg[1] geteuid.return_value = 0 try: raise PermissionError('permission denied') except PermissionError: msg = Application.permission_error(*sys.exc_info()) assert msg == ['permission denied'] def test_invalid_config(main, tmpdir): boot_path = Path(str(tmpdir)) (boot_path / 'config.txt').write_text('include syscfg.txt\n') store_path = boot_path / 'store' store_path.mkdir() def my_read(self, *args, **kwargs): self['defaults']['boot_path'] = str(boot_path) self['defaults']['store_path'] = str(store_path) self['defaults']['config_root'] = 'config.txt' return [] with mock.patch('configparser.ConfigParser.read', my_read): try: main(['set', 'video.hdmi0.group=1']) except: msg = Application.invalid_config(*sys.exc_info()) assert msg == [ "Configuration failed to validate with 1 error(s)", "video.hdmi0.mode must be 1-107 when " "video.hdmi0.group is 1", ] def test_overridden_config(main, tmpdir): boot_path = Path(str(tmpdir)) (boot_path / 'config.txt').write_text( 'include syscfg.txt\ninclude usercfg.txt\n') (boot_path / 'usercfg.txt').write_text( 'dtparam=spi=on\n') store_path = boot_path / 'store' store_path.mkdir() def my_read(self, *args, **kwargs): self['defaults']['boot_path'] = str(boot_path) self['defaults']['store_path'] = str(store_path) self['defaults']['config_root'] = 'config.txt' return [] with mock.patch('configparser.ConfigParser.read', my_read): try: main(['set', 'spi.enabled=']) except: msg = Application.overridden_config(*sys.exc_info()) assert msg == [ "Failed to set 1 setting(s)", "Expected spi.enabled to be False, but was True after being " "overridden by usercfg.txt line 1", ] def test_ineffective_config(main, tmpdir): # TODO: Improve the uncommenting code so that this test breaks and the # utility does the "right" thing (presumably warns about or deletes the # commented start_x=1 in usercfg.txt and writes it in config.txt) boot_path = Path(str(tmpdir)) (boot_path / 'config.txt').write_text('include usercfg.txt\n') (boot_path / 'usercfg.txt').write_text('#start_x=1') store_path = boot_path / 'store' store_path.mkdir() def my_read(self, *args, **kwargs): self['defaults']['boot_path'] = str(boot_path) self['defaults']['store_path'] = str(store_path) self['defaults']['config_root'] = 'config.txt' return [] with mock.patch('configparser.ConfigParser.read', my_read): try: main(['set', 'camera.enabled=on']) except: msg = Application.overridden_config(*sys.exc_info()) assert set(msg) == { "Failed to set 3 setting(s)", "Expected camera.enabled to be True, but was False with no " "valid lines; this usually means a setting like start_x or " "gpu_mem is in a file other than config.txt", "Expected boot.firmware.fixup to be fixup_x.dat, but was " "fixup.dat with no valid lines; this usually means a setting " "like start_x or gpu_mem is in a file other than config.txt", "Expected boot.firmware.filename to be start_x.elf, but was " "start.elf with no valid lines; this usually means a setting " "like start_x or gpu_mem is in a file other than config.txt", } def test_ineffective_bugs(main, tmpdir): boot_path = Path(str(tmpdir)) (boot_path / 'config.txt').write_text("""\ kernel=vmlinuz enable_uart=1 """) store_path = boot_path / 'pibootctl' store = Store(boot_path, store_path) current = store[Current] try: raise IneffectiveConfiguration( {(None, current.settings['boot.kernel.filename'])} ) except: msg = Application.overridden_config(*sys.exc_info()) assert msg == [ "Failed to set 1 setting(s)", "boot.kernel.filename appears unexpectedly as vmlinuz in the " "generated configuration; please report this bug" ] try: raise IneffectiveConfiguration( {(current.settings['serial.enabled'], None)} ) except: msg = Application.overridden_config(*sys.exc_info()) assert msg == [ "Failed to set 1 setting(s)", "serial.enabled is not set in the generated configuration " "although it was set in config.txt line 2; please report this bug" ] try: raise IneffectiveConfiguration( {(current.settings['bluetooth.enabled'], None)} ) except: msg = Application.overridden_config(*sys.exc_info()) assert msg == [ "Failed to set 1 setting(s)", "bluetooth.enabled is not set in the generated configuration; " "please report this bug" ] def test_debug_run(main, capsys): sys.excepthook = sys.__excepthook__ os.environ['DEBUG'] = '1' with pytest.raises(SystemExit): main(['help']) assert not isinstance(sys.excepthook, ErrorHandler) del os.environ['DEBUG'] with pytest.raises(SystemExit): main(['help']) assert isinstance(sys.excepthook, ErrorHandler) def test_complete_help(main): assert set(main._complete_help('he')) == {'help'} assert set(main._complete_help('cam')) == { 'camera.enabled', 'camera.led.enabled'} def test_complete_status(main): assert set(main._complete_status('he')) == set() assert set(main._complete_status('cam')) == { 'camera.enabled', 'camera.led.enabled'} def test_complete_show(main, store): assert set(main._complete_show_name('ca')) == set() store['cam'] = store[Current] store['default'] = store[Default] assert set(main._complete_show_name('ca')) == {'cam'} parsed_args = mock.Mock() parsed_args.name = 'cam' assert set(main._complete_show_vars('camera.', parsed_args)) == { 'camera.enabled', 'camera.led.enabled'} def test_complete_get(main): assert set(main._complete_get_vars('boot.kernel.a')) == { 'boot.kernel.address', 'boot.kernel.atags'} def test_complete_set(main): assert set(main._complete_set_vars('bluetooth.e')) == { 'bluetooth.enabled='} assert set(main._complete_set_vars('bluetooth.enabled=o')) == set() def test_complete_save(main, store): store['cam'] = store[Current] store['default'] = store[Default] parsed_args = mock.Mock() parsed_args.force = False assert set(main._complete_save_name('', parsed_args)) == set() parsed_args.force = True assert set(main._complete_save_name('', parsed_args)) == {'cam', 'default'} def test_complete_load(main, store): store['cam'] = store[Current] store['default'] = store[Default] assert set(main._complete_load_name('c')) == {'cam'} def test_complete_diff(main, store): store['cam'] = store[Current] store['default'] = store[Default] assert set(main._complete_diff_left('c')) == {'cam'} parsed_args = mock.Mock() parsed_args.left = 'cam' assert set(main._complete_diff_right('c', parsed_args)) == set() assert set(main._complete_diff_right('', parsed_args)) == {'default'} def test_complete_remove(main, store): store['cam'] = store[Current] store['default'] = store[Default] assert set(main._complete_remove_name('')) == {'default', 'cam'} def test_complete_rename(main, store): store['cam'] = store[Current] store['default'] = store[Default] assert set(main._complete_rename_name('')) == {'default', 'cam'} parsed_args = mock.Mock() parsed_args.force = False parsed_args.name = 'cam' assert set(main._complete_rename_to('', parsed_args)) == set() parsed_args.force = True assert set(main._complete_rename_to('', parsed_args)) == {'default'} pibootctl-0.5.2/tests/test_output.py000066400000000000000000000273331372751746400176600ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . import io import json from datetime import datetime from unittest import mock import yaml import pytest from pibootctl.store import * from pibootctl.output import * # Because pyyaml doesn't include these ... ?! def yaml_dumps(o): with io.StringIO() as buf: yaml.dump(o, buf) return buf.getvalue() def yaml_loads(s): with io.StringIO(s) as buf: return yaml.load(buf, Loader=yaml.SafeLoader) @pytest.fixture() def store(request): return [ ('foo', False, datetime(2020, 1, 1, 12, 30)), ('bar', True, datetime(2020, 1, 2, 0, 0)), ('baz', False, datetime(2020, 1, 2, 1, 0)), ] @pytest.fixture() def left_right_diff(request): left = Settings() right = left.copy() right['video.hdmi0.enabled']._value = True right['video.cec.name']._value = 'Foo' right['boot.test.enabled']._value = None diff = [ (left['video.cec.name'], right['video.cec.name']), (None, right['video.hdmi0.enabled']), (left['boot.test.enabled'], None), ] return left, right, diff def test_dump_store_user(store): buf = io.StringIO() output = Output(use_unicode=False) output.dump_store(store, buf) assert buf.getvalue() == """\ +------+--------+---------------------+ | Name | Active | Timestamp | |------+--------+---------------------| | bar | x | 2020-01-02 00:00:00 | | baz | | 2020-01-02 01:00:00 | | foo | | 2020-01-01 12:30:00 | +------+--------+---------------------+ """ buf = io.StringIO() output.dump_store([], buf) assert buf.getvalue() == "No stored boot configurations found\n" def test_dump_store_json(store): buf = io.StringIO() output = Output(style='json') output.dump_store(store, buf) assert json.loads(buf.getvalue()) == [ {'name': n, 'active': a, 'timestamp': t.isoformat()} for n, a, t in store ] def test_dump_store_yaml(store): buf = io.StringIO() output = Output(style='yaml') output.dump_store(store, buf) assert yaml_loads(buf.getvalue()) == [ {'name': n, 'active': a, 'timestamp': t} for n, a, t in store ] def test_dump_store_shell(store): buf = io.StringIO() output = Output(style='shell') output.dump_store(store, buf) assert buf.getvalue() == """\ 2020-01-01T12:30:00\tinactive\tfoo 2020-01-02T00:00:00\tactive\tbar 2020-01-02T01:00:00\tinactive\tbaz """ def test_dump_diff_user(left_right_diff): left, right, diff = left_right_diff buf = io.StringIO() output = Output(use_unicode=False) output.dump_diff('left', 'right', diff, buf) assert buf.getvalue() == """\ +---------------------+----------------+-------+ | Name | left | right | |---------------------+----------------+-------| | boot.test.enabled | off | - | | video.cec.name | 'Raspberry Pi' | 'Foo' | | video.hdmi0.enabled | - | on | +---------------------+----------------+-------+ """ buf = io.StringIO() output.dump_diff('left', 'right', [], buf) assert buf.getvalue() == 'No differences between left and right\n' def test_dump_diff_json(left_right_diff): left, right, diff = left_right_diff buf = io.StringIO() output = Output(style='json') output.dump_diff('left', 'right', diff, buf) assert json.loads(buf.getvalue()) == { 'boot.test.enabled': {'left': False}, 'video.cec.name': {'left': 'Raspberry Pi', 'right': 'Foo'}, 'video.hdmi0.enabled': {'right': True}, } def test_dump_diff_yaml(left_right_diff): left, right, diff = left_right_diff buf = io.StringIO() output = Output(style='yaml') output.dump_diff('left', 'right', diff, buf) assert yaml_loads(buf.getvalue()) == { 'boot.test.enabled': {'left': False}, 'video.cec.name': {'left': 'Raspberry Pi', 'right': 'Foo'}, 'video.hdmi0.enabled': {'right': True}, } def test_dump_diff_shell(left_right_diff): left, right, diff = left_right_diff buf = io.StringIO() output = Output(style='shell') output.dump_diff('left', 'right', diff, buf) assert set(buf.getvalue().splitlines()) == { "boot.test.enabled\tfalse\t-", "video.cec.name\t'Raspberry Pi'\tFoo", "video.hdmi0.enabled\t-\ttrue", } def test_dump_settings_user(): # Cut down the settings to something manageable for this test default = Settings().filter('video.cec.*') default['video.cec.name']._value = 'Foo' buf = io.StringIO() output = Output(use_unicode=False) output.dump_settings(default, buf) assert buf.getvalue() == """\ +-------------------+-------+ | Name | Value | |-------------------+-------| | video.cec.enabled | on | | video.cec.init | on | | video.cec.name | 'Foo' | +-------------------+-------+ """ buf = io.StringIO() output.dump_settings(default, buf, mod_only=False) assert buf.getvalue() == """\ +-------------------+----------+-------+ | Name | Modified | Value | |-------------------+----------+-------| | video.cec.enabled | | on | | video.cec.init | | on | | video.cec.name | x | 'Foo' | +-------------------+----------+-------+ """ buf = io.StringIO() output.dump_settings(set(), buf) assert buf.getvalue() == ( "No modified settings matching the pattern found.\n" "Try --all to include unmodified settings.\n") buf = io.StringIO() output.dump_settings(set(), buf, mod_only=False) assert buf.getvalue() == ( "No modified settings matching the pattern found.\n") def test_dump_settings_json(): default = Settings() buf = io.StringIO() output = Output(style='json') output.dump_settings(default, buf) assert json.loads(buf.getvalue()) == { setting.name: setting.value for setting in default.values() } def test_dump_settings_yaml(): default = Settings() buf = io.StringIO() output = Output(style='yaml') output.dump_settings(default, buf) assert yaml_loads(buf.getvalue()) == { setting.name: setting.value for setting in default.values() } def test_dump_settings_shell(): # Cut down the settings to something manageable for this test default = Settings().filter('video.cec.*') buf = io.StringIO() output = Output(style='shell') output.dump_settings(default, buf) # Sets because there's no guarantee of order in the output assert set(buf.getvalue().splitlines()) == { "video_cec_enabled=true", "video_cec_init=true", "video_cec_name='Raspberry Pi'", } def test_load_settings_user(): output = Output() with pytest.raises(NotImplementedError): output.load_settings(io.StringIO()) def test_load_settings_json(): settings = { 'video.cec.enabled': True, 'video.cec.init': False, 'video.cec.name': 'Raspberry Pi', } buf = io.StringIO() json.dump(settings, buf) buf.seek(0) output = Output(style='json') assert output.load_settings(buf) == settings def test_load_settings_yaml(): settings = { 'video.cec.enabled': True, 'video.cec.init': False, 'video.cec.name': 'Raspberry Pi', } buf = io.StringIO() yaml.dump(settings, buf) buf.seek(0) output = Output(style='yaml') assert output.load_settings(buf) == settings def test_load_settings_shell(): settings = { 'video.cec.enabled': True, 'video.cec.init': False, 'video.cec.name': 'Raspberry Pi', 'gpu.mem': 256, 'made.up.value': [256, False, 'Raspberry Pi'], } buf = io.StringIO("""\ video_cec_enabled=true video_cec_init=false video_cec_name='Raspberry Pi' gpu_mem=256 made_up_value=(256 false 'Raspberry Pi') """) output = Output(style='shell') assert output.load_settings(buf) == settings buf = io.StringIO("false") assert output.load_settings(buf) is False buf = io.StringIO("gpu_mem=256") assert output.load_settings(buf) == {'gpu.mem': 256} def test_format_value_user(): output = Output(use_unicode=False) assert output.format_value(1) == '1' assert output.format_value(None) == 'auto' assert output.format_value(True) == 'on' assert output.format_value(False) == 'off' assert output.format_value([1, 2, 3]) == repr([1, 2, 3]) assert output.format_value('Foo') == repr('Foo') assert output.format_value('Foo Bar') == repr('Foo Bar') def test_format_value_json(): output = Output(style='json') assert output.format_value(1) == json.dumps(1) assert output.format_value(None) == json.dumps(None) assert output.format_value(True) == json.dumps(True) assert output.format_value(False) == json.dumps(False) assert output.format_value([1, 2, 3]) == json.dumps([1, 2, 3]) assert output.format_value('Foo') == json.dumps('Foo') assert output.format_value('Foo Bar') == json.dumps('Foo Bar') def test_format_value_yaml(): output = Output(style='yaml') assert output.format_value(1) == yaml_dumps(1) assert output.format_value(None) == yaml_dumps(None) assert output.format_value(True) == yaml_dumps(True) assert output.format_value(False) == yaml_dumps(False) assert output.format_value([1, 2, 3]) == yaml_dumps([1, 2, 3]) assert output.format_value('Foo') == yaml_dumps('Foo') assert output.format_value('Foo Bar') == yaml_dumps('Foo Bar') def test_format_value_shell(): output = Output(style='shell') assert output.format_value(1) == '1' assert output.format_value(None) == 'auto' assert output.format_value(True) == 'true' assert output.format_value(False) == 'false' assert output.format_value([1, 2, 3]) == '(1 2 3)' assert output.format_value('Foo') == 'Foo' assert output.format_value('Foo Bar') == "'Foo Bar'" def test_dump_setting_user(): with mock.patch('pibootctl.output.term_size') as term_size: term_size.return_value = (80, 24) default = Settings() buf = io.StringIO() output = Output(use_unicode=False) output.dump_setting(default['video.cec.name'], buf) assert buf.getvalue() == """\ Name: video.cec.name Default: 'Raspberry Pi' Command(s): cec_osd_name The name the Pi (as a CEC device) should provide to the connected display; defaults to "Raspberry Pi". """ buf = io.StringIO() output = Output(use_unicode=False) output.dump_setting(default['i2c.baud'], buf) assert buf.getvalue() == """\ Name: i2c.baud Default: 100000 Overlay: base Parameter: i2c_arm_baudrate The baud-rate of the ARM I2C bus. """ buf = io.StringIO() output = Output(use_unicode=False) output.dump_setting(default['bluetooth.enabled'], buf) assert buf.getvalue() == """\ Name: bluetooth.enabled Default: {value} Controls whether the Bluetooth module (Raspberry Pi 3 and later, and the Raspberry Pi Zero W), is enabled (which it is by default). Note that disabling the module can affect the default state of serial.enabled and serial.uart. """.format(value='on' if default['bluetooth.enabled'].default else 'off') pibootctl-0.5.2/tests/test_parser.py000066400000000000000000000520411372751746400176060ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2019, 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . import zipfile import warnings from pathlib import Path from unittest import mock from hashlib import sha1 from datetime import datetime import pytest from pibootctl.parser import * cond_all = BootConditions() cond_none = cond_all.evaluate('none') def test_coalesce(): assert coalesce(1) == 1 assert coalesce(None, 1) == 1 assert coalesce(None, None, 1) == 1 assert coalesce() is None def test_str(): assert str(BootSection('config.txt', 1, cond_all, 'all')) == '[all]' assert str(BootCommand( 'config.txt', 1, cond_all, 'initramfs', ('initrd.img', 'followkernel') )) == 'initramfs initrd.img followkernel' assert str(BootCommand('config.txt', 1, cond_all, 'hdmi_group', '1')) == 'hdmi_group=1' assert str(BootCommand('config.txt', 1, cond_all, 'hdmi_group', '2', 1)) == 'hdmi_group:1=2' assert str(BootInclude('config.txt', 1, cond_all, 'syscfg.txt')) == 'include syscfg.txt' assert str(BootOverlay('config.txt', 1, cond_all, 'foo')) == 'dtoverlay=foo' assert str(BootParam('config.txt', 1, cond_all, 'base', 'spi', 'on')) == 'dtparam=spi=on' def test_repr(): assert repr(BootComment('config.txt', 2, cond_all)) == ( "BootComment(filename='config.txt', linenum=2, comment=None)") assert repr(BootSection('config.txt', 1, cond_all, 'all')) == ( "BootSection(filename='config.txt', linenum=1, section='all')") assert repr(BootCommand( 'config.txt', 1, cond_all, 'initramfs', ('initrd.img', 'followkernel') )) == ( "BootCommand(filename='config.txt', linenum=1, " "command='initramfs', params=('initrd.img', 'followkernel'), " "hdmi=None)" ) assert repr(BootCommand('config.txt', 1, cond_all, 'hdmi_group', '1')) == ( "BootCommand(filename='config.txt', linenum=1, " "command='hdmi_group', params='1', hdmi=None)") assert repr(BootCommand('config.txt', 1, cond_all, 'hdmi_group', '2', 1)) == ( "BootCommand(filename='config.txt', linenum=1, " "command='hdmi_group', params='2', hdmi=1)") assert repr(BootInclude('config.txt', 1, cond_all, 'syscfg.txt')) == ( "BootInclude(filename='config.txt', linenum=1, " "include='syscfg.txt')") assert repr(BootOverlay('config.txt', 1, cond_all, 'foo')) == ( "BootOverlay(filename='config.txt', linenum=1, overlay='foo')") assert repr(BootParam('config.txt', 1, cond_all, 'base', 'spi', 'on')) == ( "BootParam(filename='config.txt', linenum=1, overlay='base', " "param='spi', value='on')") def test_bootline_comparisons(): assert BootComment('config.txt', 1, cond_all, 'foo') != 1 assert BootComment('config.txt', 1, cond_all, 'foo') != \ BootComment('config.txt', 2, cond_all, 'foo') assert BootComment('config.txt', 1, cond_all, 'foo') != \ BootComment('config.txt', 1, cond_none, 'foo') assert BootComment('config.txt', 1, cond_all, 'foo') != \ BootComment('config.txt', 1, cond_all, 'bar') assert BootComment('config.txt', 1, cond_all, 'foo') == \ BootComment('config.txt', 1, cond_all, 'foo') def test_bootsection_comparisons(): assert BootComment('config.txt', 1, cond_all, 'foo') != \ BootSection('config.txt', 1, cond_all, 'all') assert BootSection('config.txt', 1, cond_all, 'all') != \ BootComment('config.txt', 1, cond_all, 'foo') assert BootSection('config.txt', 1, cond_all, 'foo') != \ BootSection('config.txt', 1, cond_all, 'bar') def test_bootcommand_comparisons(): assert BootCommand('config.txt', 1, cond_all, 'disable_overscan', '1') != \ BootComment('config.txt', 1, cond_all, 'foo') assert BootCommand('config.txt', 1, cond_all, 'disable_overscan', '1', 0) != \ BootCommand('config.txt', 1, cond_all, 'disable_overscan', '1', 1) assert BootCommand('config.txt', 1, cond_all, 'disable_overscan', '1') != \ BootCommand('config.txt', 1, cond_all, 'disable_overscan', '0') assert BootCommand('config.txt', 1, cond_all, 'hdmi_mode', '1') != \ BootCommand('config.txt', 1, cond_all, 'disable_overscan', '1') def test_bootinclude_comparisons(): assert BootInclude('config.txt', 1, cond_all, 'foo.txt') != \ BootComment('config.txt', 1, cond_all, 'foo') assert BootInclude('config.txt', 1, cond_all, 'foo.txt') != \ BootInclude('config.txt', 1, cond_all, 'bar.txt') def test_bootoverlay_comparisons(): assert BootOverlay('config.txt', 1, cond_all, 'gpio-shutdown') != \ BootComment('config.txt', 1, cond_all, 'foo') assert BootOverlay('config.txt', 1, cond_all, 'gpio-shutdown') != \ BootOverlay('config.txt', 1, cond_all, 'gpio-poweroff') assert BootParam('config.txt', 1, cond_all, 'gpio-shutdown', 'gpio_pin', 4) != \ BootOverlay('config.txt', 1, cond_all, 'gpio-shutdown') assert BootParam('config.txt', 1, cond_all, 'gpio-shutdown', 'gpio_pin', 4) != \ BootParam('config.txt', 1, cond_all, 'gpio-poweroff', 'gpiopin', 4) assert BootParam('config.txt', 1, cond_all, 'gpio-shutdown', 'gpio_pin', 4) != \ BootParam('config.txt', 1, cond_all, 'gpio-shutdown', 'gpio_pin', 17) def test_bootconditions_comparisons(): cond_pi3 = cond_all.evaluate('pi3') cond_pi3p = cond_all.evaluate('pi3+') cond_gpio = cond_pi3.evaluate('gpio4=1') cond_edid = cond_pi3.evaluate('EDID=foo') cond_hdmi = cond_pi3.evaluate('HDMI:1') cond_serial = cond_pi3.evaluate('0xf000000d') assert cond_all != 1 with pytest.raises(TypeError): cond_all < 1 assert cond_pi3 != cond_all assert cond_pi3 != cond_gpio assert cond_pi3 != cond_edid assert cond_pi3 != cond_hdmi assert cond_pi3 != cond_serial assert cond_pi3 <= cond_all assert cond_pi3 < cond_all assert cond_pi3p != cond_pi3 assert cond_pi3p < cond_pi3 assert cond_gpio < cond_pi3 assert cond_edid < cond_pi3 assert cond_hdmi < cond_pi3 assert cond_serial < cond_pi3 assert cond_pi3 > cond_pi3p assert cond_pi3 > cond_gpio assert cond_pi3 > cond_serial assert cond_pi3 > cond_edid assert cond_pi3 > cond_hdmi def test_bootconditions_generate(): cond_pi3 = cond_all.evaluate('pi3') cond_pi3p = cond_all.evaluate('pi3+') cond_gpio = cond_pi3.evaluate('gpio4=1') cond_edid = cond_pi3.evaluate('EDID=foo') cond_hdmi = cond_pi3.evaluate('HDMI:1') cond_serial = cond_pi3.evaluate('0xf000000d') assert list(cond_pi3.generate()) == ['[pi3]'] assert list(cond_pi3p.generate()) == ['[pi3+]'] assert list(cond_gpio.generate()) == ['[pi3]', '[gpio4=1]'] assert list(cond_edid.generate()) == ['[pi3]', '[EDID=foo]'] assert list(cond_hdmi.generate()) == ['[pi3]', '[HDMI:1]'] assert list(cond_serial.generate()) == ['[pi3]', '[0xF000000D]'] def test_parse_basic(tmpdir): tmpdir.join('config.txt').write("""\ # This is a comment kernel=vmlinuz initramfs initrd.img followkernel device_tree_address=0x3000000 dtoverlay=vc4-fkms-v3d """) p = BootParser(str(tmpdir)) p.parse() assert p.config == [ BootComment('config.txt', 1, cond_all, comment=' This is a comment'), BootCommand('config.txt', 2, cond_all, 'kernel', 'vmlinuz'), BootCommand('config.txt', 3, cond_all, 'initramfs', ('initrd.img', 'followkernel')), BootCommand('config.txt', 4, cond_all, 'device_tree_address', '0x3000000'), BootOverlay('config.txt', 5, cond_all, 'vc4-fkms-v3d'), ] def test_parse_invalid(tmpdir): tmpdir.join('config.txt').write("""\ # This is a comment This is not """) p = BootParser(str(tmpdir)) with pytest.warns(BootInvalid) as w: p.parse() assert p.config == [ BootComment('config.txt', 1, cond_all, comment=' This is a comment'), ] assert len(w) == 1 assert w[0].message.args[0] == 'config.txt:2 invalid line' def test_parse_overlay_and_params(tmpdir): tmpdir.join('config.txt').write("""\ dtparam=audio=on,i2c=on,i2c_baudrate=400000 dtparam=spi,i2c0 dtoverlay=lirc-rpi:gpio_out_pin=16,gpio_in_pin=17,gpio_in_pull=down """) p = BootParser(str(tmpdir)) p.parse() assert p.config == [ BootParam('config.txt', 1, cond_all, 'base', 'audio', 'on'), BootParam('config.txt', 1, cond_all, 'base', 'i2c_arm', 'on'), BootParam('config.txt', 1, cond_all, 'base', 'i2c_arm_baudrate', '400000'), BootParam('config.txt', 2, cond_all, 'base', 'spi', 'on'), BootParam('config.txt', 2, cond_all, 'base', 'i2c_vc', 'on'), BootOverlay('config.txt', 3, cond_all, 'lirc-rpi'), BootParam('config.txt', 3, cond_all, 'lirc-rpi', 'gpio_out_pin', '16'), BootParam('config.txt', 3, cond_all, 'lirc-rpi', 'gpio_in_pin', '17'), BootParam('config.txt', 3, cond_all, 'lirc-rpi', 'gpio_in_pull', 'down'), ] def test_parse_include(tmpdir): tmpdir.join('config.txt').write("""\ # This is a comment [none] dtoverlay=vc4-fkms-v3d [all] dtoverlay=foo include syscfg.txt """) tmpdir.join('syscfg.txt').write("""\ dtparam=i2c=on dtparam=spi=on hdmi_group=1 hdmi_mode=4 """) p = BootParser(str(tmpdir)) p.parse() assert p.config == [ BootComment('config.txt', 1, cond_all, comment=' This is a comment'), BootSection('config.txt', 2, cond_none, 'none'), BootOverlay('config.txt', 3, cond_none, 'vc4-fkms-v3d'), BootSection('config.txt', 5, cond_all, 'all'), BootOverlay('config.txt', 6, cond_all, 'foo'), BootInclude('config.txt', 7, cond_all, 'syscfg.txt'), BootParam('syscfg.txt', 1, cond_all, 'base', 'i2c_arm', 'on'), BootParam('syscfg.txt', 2, cond_all, 'base', 'spi', 'on'), BootCommand('syscfg.txt', 4, cond_all, 'hdmi_group', '1'), BootCommand('syscfg.txt', 5, cond_all, 'hdmi_mode', '4'), ] def test_parse_suppressed_includes(tmpdir): tmpdir.join('config.txt').write("""\ # This is a comment [none] dtoverlay=vc4-fkms-v3d include inc1.txt [all] dtoverlay=foo """) tmpdir.join('inc1.txt').write("""\ [all] dtparam=i2c=on dtparam=spi=on include inc2.txt """) tmpdir.join('inc2.txt').write("""\ hdmi_group=1 hdmi_mode=4 """) p = BootParser(str(tmpdir)) p.parse() cond_inc1 = cond_all._replace(suppress_count=1) cond_inc2 = cond_all._replace(suppress_count=2) assert p.config == [ BootComment('config.txt', 1, cond_all, comment=' This is a comment'), BootSection('config.txt', 2, cond_none, 'none'), BootOverlay('config.txt', 3, cond_none, 'vc4-fkms-v3d'), BootInclude('config.txt', 4, cond_none, 'inc1.txt'), BootSection('inc1.txt', 1, cond_inc1, 'all'), BootParam('inc1.txt', 2, cond_inc1, 'base', 'i2c_arm', 'on'), BootParam('inc1.txt', 3, cond_inc1, 'base', 'spi', 'on'), BootInclude('inc1.txt', 4, cond_inc2, 'inc2.txt'), BootCommand('inc2.txt', 1, cond_inc2, 'hdmi_group', '1'), BootCommand('inc2.txt', 2, cond_inc2, 'hdmi_mode', '4'), BootSection('config.txt', 6, cond_all, 'all'), BootOverlay('config.txt', 7, cond_all, 'foo'), ] def test_parse_hdmi_section(tmpdir): tmpdir.join('config.txt').write("""\ # This is a comment hdmi_group=1 hdmi_mode=4 [HDMI:1] hdmi_group=2 hdmi_mode=28 [HDMI:foo] """) p = BootParser(str(tmpdir)) p.parse() cond_hdmi1 = cond_all._replace(hdmi=1) assert p.config == [ BootComment('config.txt', 1, cond_all, comment=' This is a comment'), BootCommand('config.txt', 2, cond_all, 'hdmi_group', '1'), BootCommand('config.txt', 3, cond_all, 'hdmi_mode', '4'), BootSection('config.txt', 5, cond_hdmi1, 'HDMI:1'), BootCommand('config.txt', 6, cond_hdmi1, 'hdmi_group', '2', 1), BootCommand('config.txt', 7, cond_hdmi1, 'hdmi_mode', '28', 1), BootSection('config.txt', 9, cond_hdmi1, 'HDMI:foo'), ] def test_parse_hdmi_suffix(tmpdir): tmpdir.join('config.txt').write("""\ hdmi_group:0=1 hdmi_mode:1=4 hdmi_mode:a=4 """) p = BootParser(str(tmpdir)) p.parse() assert p.config == [ BootCommand('config.txt', 1, cond_all, 'hdmi_group', '1', 0), BootCommand('config.txt', 2, cond_all, 'hdmi_mode', '4', 1), BootCommand('config.txt', 3, cond_all, 'hdmi_mode', '4'), ] def test_parse_edid_section(tmpdir): tmpdir.join('config.txt').write("""\ # This is a comment [EDID=BNQ-BenQ_GW2270] hdmi_group=1 hdmi_mode=16 [EDID=VSC-TD2220] hdmi_group=1 hdmi_mode=4 """) p = BootParser(str(tmpdir)) p.parse() cond_edid1 = cond_all._replace(edid='BNQ-BenQ_GW2270') cond_edid2 = cond_all._replace(edid='VSC-TD2220') assert p.config == [ BootComment('config.txt', 1, cond_all, comment=' This is a comment'), BootSection('config.txt', 2, cond_edid1, 'EDID=BNQ-BenQ_GW2270'), BootCommand('config.txt', 3, cond_edid1, 'hdmi_group', '1'), BootCommand('config.txt', 4, cond_edid1, 'hdmi_mode', '16'), BootSection('config.txt', 6, cond_edid2, 'EDID=VSC-TD2220'), BootCommand('config.txt', 7, cond_edid2, 'hdmi_group', '1'), BootCommand('config.txt', 8, cond_edid2, 'hdmi_mode', '4'), ] def test_parse_gpio_section(tmpdir): tmpdir.join('config.txt').write("""\ # This is a comment [gpio4=1] dtparam=audio=on [gpiofoo=bar] """) p = BootParser(str(tmpdir)) p.parse() cond_gpio = cond_all._replace(gpio=(4, True)) assert p.config == [ BootComment('config.txt', 1, cond_all, comment=' This is a comment'), BootSection('config.txt', 2, cond_gpio, 'gpio4=1'), BootParam('config.txt', 3, cond_gpio, 'base', 'audio', 'on'), BootSection('config.txt', 5, cond_gpio, 'gpiofoo=bar'), ] def test_parse_serial_bad_section(tmpdir): with mock.patch('pibootctl.parser.get_board_serial') as get_board_serial: tmpdir.join('config.txt').write("""\ # This is a comment [0xwtf] dtparam=audio=on """) p = BootParser(str(tmpdir)) p.parse() assert p.config == [ BootComment('config.txt', 1, cond_all, comment=' This is a comment'), BootSection('config.txt', 2, cond_all, '0xwtf'), BootParam('config.txt', 3, cond_all, 'base', 'audio', 'on'), ] def test_parse_unknown_section(tmpdir, recwarn): tmpdir.join('config.txt').write("""\ # This is a comment [foo] dtparam=audio=on """) p = BootParser(str(tmpdir)) p.parse() assert p.config == [ BootComment('config.txt', 1, cond_all, comment=' This is a comment'), BootSection('config.txt', 2, cond_all, 'foo'), BootParam('config.txt', 3, cond_all, 'base', 'audio', 'on'), ] assert len(recwarn) == 1 assert recwarn.pop(BootInvalid) def test_parse_serial_section_match(tmpdir): with mock.patch('pibootctl.parser.get_board_serial') as get_board_serial: get_board_serial.return_value = 0xdeadd00d tmpdir.join('config.txt').write("""\ # This is a comment [0xdeadd00d] dtparam=audio=on """) p = BootParser(str(tmpdir)) p.parse() cond_serial = cond_all._replace(serial=0xdeadd00d) assert p.config == [ BootComment('config.txt', 1, cond_all, comment=' This is a comment'), BootSection('config.txt', 2, cond_serial, '0xdeadd00d'), BootParam('config.txt', 3, cond_serial, 'base', 'audio', 'on'), ] assert cond_serial.enabled def test_parse_serial_section_mismatch(tmpdir): with mock.patch('pibootctl.parser.get_board_serial') as get_board_serial: get_board_serial.return_value = 0xdeadd00d tmpdir.join('config.txt').write("""\ # This is a comment [0x12345678] dtparam=audio=on """) p = BootParser(str(tmpdir)) p.parse() cond_serial = cond_all._replace(serial=0x12345678) assert p.config == [ BootComment('config.txt', 1, cond_all, comment=' This is a comment'), BootSection('config.txt', 2, cond_serial, '0x12345678'), BootParam('config.txt', 3, cond_serial, 'base', 'audio', 'on'), ] assert not cond_serial.enabled def test_parse_pi_section(tmpdir): with mock.patch('pibootctl.parser.get_board_types') as get_board_types: get_board_types.return_value = {'pi3', 'pi3+'} tmpdir.join('config.txt').write("""\ # This is a comment [pi2] kernel=uboot_2.bin [pi3] kernel=uboot_3_32b.bin [pi4] kernel=uboot_4_32b.bin [pi400] """) p = BootParser(str(tmpdir)) p.parse() cond_pi2 = cond_all._replace(pi='pi2') cond_pi3 = cond_all._replace(pi='pi3') cond_pi4 = cond_all._replace(pi='pi4') assert p.config == [ BootComment('config.txt', 1, cond_all, comment=' This is a comment'), BootSection('config.txt', 2, cond_pi2, 'pi2'), BootCommand('config.txt', 3, cond_pi2, 'kernel', 'uboot_2.bin'), BootSection('config.txt', 4, cond_pi3, 'pi3'), BootCommand('config.txt', 5, cond_pi3, 'kernel', 'uboot_3_32b.bin'), BootSection('config.txt', 6, cond_pi4, 'pi4'), BootCommand('config.txt', 7, cond_pi4, 'kernel', 'uboot_4_32b.bin'), BootSection('config.txt', 8, cond_pi4, 'pi400'), ] assert not cond_pi2.enabled assert not cond_pi4.enabled assert cond_pi3.enabled def test_parse_attr(tmpdir): tmpdir = Path(str(tmpdir)) content = b"""\ # This is a comment [pi2] kernel=uboot_2.bin [pi3] kernel=uboot_3_32b.bin [pi4] kernel=uboot_4_32b.bin """ with mock.patch('pibootctl.parser.get_board_types') as get_board_types: get_board_types.return_value = {'pi3', 'pi3+'} (tmpdir / 'config.txt').write_bytes(content) p = BootParser(tmpdir) p.parse() assert p.files == { 'config.txt': BootFile( 'config.txt', datetime.fromtimestamp((tmpdir / 'config.txt').stat().st_mtime), content, 'ascii', 'replace' ) } h = sha1() h.update(content) assert p.hash == h.hexdigest().lower() def test_parse_store(tmpdir): data1 = b"""\ # This is a comment kernel=vmlinuz device_tree_address=0x3000000 dtoverlay=vc4-fkms-v3d """ data2 = b'quiet splash' with zipfile.ZipFile(str(tmpdir.join('stored.zip')), 'w') as arc: arc.writestr('config.txt', data1) arc.writestr('cmdline.txt', data2) h = hashlib.sha1() h.update(data1) h.update(data2) with zipfile.ZipFile(str(tmpdir.join('stored.zip')), 'r') as arc: p = BootParser(arc) p.parse() p.add('cmdline.txt') assert p.config == [ BootComment('config.txt', 1, cond_all, comment=' This is a comment'), BootCommand('config.txt', 2, cond_all, 'kernel', 'vmlinuz'), BootCommand('config.txt', 3, cond_all, 'device_tree_address', '0x3000000'), BootOverlay('config.txt', 4, cond_all, 'vc4-fkms-v3d'), ] assert p.hash == h.hexdigest().lower() def test_add_to_zip(tmpdir): tmpdir.join('config.txt').write("""\ # This is a comment kernel=vmlinuz device_tree_address=0x3000000 dtoverlay=vc4-fkms-v3d """) tmpdir.join('cmdline.txt').write("quiet splash") p1 = BootParser(str(tmpdir)) p1.parse() with zipfile.ZipFile(str(tmpdir.join('stored.zip')), 'w') as arc: for f in p1.files.values(): f.add_to_zip(arc) with zipfile.ZipFile(str(tmpdir.join('stored.zip')), 'r') as arc: p2 = BootParser(arc) p2.parse() assert p1.hash == p2.hash assert p1.files.keys() == p2.files.keys() for f in p1.files: assert p1.files[f].content == p2.files[f].content def test_parse_dict(tmpdir): tmpdir = Path(str(tmpdir)) (tmpdir / 'config.txt').write_text("""\ # This is a comment [all] dtoverlay=foo include syscfg.txt """) (tmpdir / 'syscfg.txt').write_text("""\ dtparam=i2c=on dtparam=spi=on hdmi_group=1 hdmi_mode=4 """) (tmpdir / 'edid.dat').write_bytes(b'\x00\x00\x00\xFF') p1 = BootParser(tmpdir) p1.parse() p1.add('edid.dat') masked = p1.files.copy() del masked['syscfg.txt'] p2 = BootParser(masked) p2.parse() p2.add('edid.dat') assert p2.config == [ BootComment('config.txt', 1, cond_all, comment=' This is a comment'), BootSection('config.txt', 2, cond_all, 'all'), BootOverlay('config.txt', 3, cond_all, 'foo'), BootInclude('config.txt', 4, cond_all, 'syscfg.txt'), ] def test_parse_empty(tmpdir): p = BootParser(str(tmpdir)) p.parse() assert p.config == [] assert p.hash == hashlib.sha1().hexdigest().lower() pibootctl-0.5.2/tests/test_setting.py000066400000000000000000001707251372751746400200010ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . from unittest import mock from itertools import chain from operator import attrgetter import pytest from pibootctl.userstr import UserStr from pibootctl.store import Settings from pibootctl.parser import * from pibootctl.setting import * cond_all = BootConditions() def make_settings(*specs): return Settings({spec.name: spec for spec in specs}) @pytest.fixture() def fw_settings(request): return make_settings( CommandFirmwareCamera( 'camera.enabled', commands=('start_x', 'start_debug')), CommandFirmwareDebug( 'boot.debug.enabled', commands=('start_debug', 'start_file', 'fixup_file')), CommandGPUMem( 'gpu.mem', default=64, commands=('gpu_mem', 'gpu_mem_256', 'gpu_mem_512', 'gpu_mem_1024')), CommandFirmwareFilename('boot.firmware.filename', command='start_file'), CommandFirmwareFixup('boot.firmware.fixup', command='fixup_file')) def test_setting_init(): s = Setting('foo.bar', default='baz') assert s.update('quux') == 'quux' assert repr(s) == "" with pytest.raises(NotImplementedError): s.extract(None) with pytest.raises(NotImplementedError): s.output() with pytest.raises(NotImplementedError): s.key def test_setting_override(): s = Setting('foo.bar', default='baz') with s._override('quux'): assert s.value == 'quux' assert s.value == 'baz' def test_overlay_init(): o = Overlay('sense.enabled', overlay='sensehat') assert o.overlay == 'sensehat' assert o.key == ('overlays', 'sensehat') assert o.update(False) is False def test_overlay_extract(): o = Overlay('sense.enabled', overlay='sensehat') config = [BootParam('config.txt', 3, cond_all, 'base', 'i2c_arm', 'on')] assert list(o.extract(config)) == [] config = [ BootOverlay('config.txt', 3, cond_all, 'disable-bt'), BootOverlay('config.txt', 4, cond_all, 'sensehat')] assert list(o.extract(config)) == [(config[1], True)] def test_overlay_output(): o = Overlay('sense.enabled', overlay='sensehat') assert list(o.output()) == [] o._value = True assert list(o.output()) == ['dtoverlay=sensehat'] def test_param_init(): p = OverlayParamStr('i2c.enabled', param='i2c_arm') assert p.overlay == 'base' assert p.param == 'i2c_arm' assert p.key == ('overlays', '', 'i2c_arm') assert p.update('foo') == 'foo' def test_param_extract(): p = OverlayParamStr('i2c.enabled', param='i2c_arm') config = [ BootOverlay('config.txt', 1, cond_all, 'sensehat'), BootOverlay('config.txt', 2, cond_all, 'base'), BootParam('config.txt', 3, cond_all, 'base', 'i2c_arm', 'on'), ] assert list(p.extract(config)) == [ (config[1], None), (config[2], 'on'), ] def test_param_output(): p = OverlayParamStr('i2c.enabled', param='i2c_arm') assert list(p.output()) == [] p._value = 'on' assert list(p.output()) == ['dtparam=i2c_arm=on'] def test_param_validate(): p = OverlayParamStr('usb.dwc2.mode', overlay='dwc2', param='dr_mode', default='otg', valid={ 'host': 'Host mode always', 'peripheral': 'Device mode always', 'otg': 'Host/device mode', }) p._value = 'host' p.validate() assert p.hint == 'Host mode always' p._value = 'foo' with pytest.raises(ValueError): p.validate() def test_int_param_init(): p = OverlayParamInt('i2c.baud', param='i2c_baud') assert p.update('100000') == 100000 assert p.update('0x1c200') == 115200 def test_int_param_extract(): p = OverlayParamInt('i2c.baud', param='i2c_baud') config = [ BootOverlay('config.txt', 1, cond_all, 'sensehat'), BootOverlay('config.txt', 2, cond_all, 'base'), BootParam('config.txt', 3, cond_all, 'base', 'i2c_baud', '100000'), BootParam('config.txt', 3, cond_all, 'base', 'i2c_baud', 'foo'), ] with warnings.catch_warnings(record=True) as w: assert list(p.extract(config)) == [ (config[1], None), (config[2], 100000), (config[3], None), ] assert len(w) == 1 assert issubclass(w[0].category, ParseWarning) def test_int_param_validate(): p = OverlayParamInt('draws.ch4.gain', overlay='draws', param='draws_adc_ch4_gain', valid={ 0: '+/- 6.144V', 1: '+/- 4.096V', 2: '+/- 2.048V', 3: '+/- 1.024V', 4: '+/- 0.512V', 5: '+/- 0.256V', 6: '+/- 0.256V', 7: '+/- 0.256V', }) p._value = 4 p.validate() assert p.hint == '+/- 0.512V' p._value = 8 with pytest.raises(ValueError): p.validate() def test_bool_param_init(): p = OverlayParamBool('i2c.enabled', param='i2c_arm') assert p.update(UserStr('on')) is True assert p.update(1) is True def test_bool_param_extract(): p = OverlayParamBool('i2c.enabled', param='i2c_arm') config = [ BootOverlay('config.txt', 1, cond_all, 'sensehat'), BootOverlay('config.txt', 2, cond_all, 'base'), BootParam('config.txt', 3, cond_all, 'base', 'i2c_arm', 'on'), ] assert list(p.extract(config)) == [ (config[1], None), (config[2], True), ] def test_bool_param_output(): p = OverlayParamBool('i2c.enabled', param='i2c_arm') assert list(p.output()) == [] p._value = True assert list(p.output()) == ['dtparam=i2c_arm=on'] def test_command_init(): c = CommandStr('video.cec.name', commands=('foo', 'bar'), default='RPi', index=1) assert c.commands == ('foo', 'bar') assert c.index == 1 assert c.hint is None c = Command('video.cec.name', command='cec_osd_name', default='RPi') assert c.commands == ('cec_osd_name',) assert c.index is None assert c.key == ('commands', 'video.cec.name') def test_command_extract(): c = CommandStr('video.cec.name', command='cec_osd_name', default='RPi') config = [ BootCommand('config.txt', 1, cond_all, 'cec_osd_name', 'FOO', hdmi=0), ] assert list(c.extract(config)) == [ (config[0], 'FOO'), ] def test_command_output(): c = CommandStr('video.cec.name', command='cec_osd_name', default='RPi') assert list(c.output()) == [] c._value = 'FOO' assert list(c.output()) == ['cec_osd_name=FOO'] c = Command('video.cec.name', command='cec_osd_name', default='RPi', index=1) c._value = 'FOO' assert list(c.output()) == ['cec_osd_name:1=FOO'] def test_str_command_init(): c = CommandStr('gpio.1.pull', command='gpio0pull', default='np', valid={'up': 'pull up', 'dn': 'pull down', 'np': 'no pull'}) assert c.commands == ('gpio0pull',) assert c.default == 'np' assert c.hint == 'no pull' c.validate() c._value = 'up' assert c.hint == 'pull up' c.validate() c._value = 'foo' with pytest.raises(ValueError): c.validate() def test_int_command_init(): c = CommandInt('video.hdmi1.drive', index=1, command='hdmi_drive', valid={0: 'auto', 1: 'dvi', 2: 'hdmi'}) assert c.commands == ('hdmi_drive',) assert c.index == 1 assert c.default == 0 assert c.hint == 'auto' c.validate() c._value = 1 assert c.hint == 'dvi' c.validate() c._value = 4 with pytest.raises(ValueError): c.validate() def test_int_command_extract(): c = CommandInt('video.hdmi1.drive', index=1, command='hdmi_drive', valid={0: 'auto', 1: 'dvi', 2: 'hdmi'}) config = [ BootCommand('config.txt', 1, cond_all, 'hdmi_drive', '2', hdmi=1), BootCommand('config.txt', 1, cond_all, 'hdmi_drive', 'foo', hdmi=1), ] with warnings.catch_warnings(record=True) as w: assert list(c.extract(config)) == [ (config[0], 2), (config[1], None), ] assert len(w) == 1 assert issubclass(w[0].category, ParseWarning) def test_int_command_output(): c = CommandInt('video.hdmi1.drive', index=1, command='hdmi_drive', valid={0: 'auto', 1: 'dvi', 2: 'hdmi'}) assert list(c.output()) == [] c._value = 2 assert list(c.output()) == ['hdmi_drive:1=2'] def test_hex_command_init(): c = CommandIntHex('boot.dt.address', command='dt_address') assert c.commands == ('dt_address',) assert c.index == 0 assert c.default == 0 assert c.hint == '0x0' c._value = 0x3000000 assert c.hint == '0x3000000' def test_hex_command_output(): c = CommandIntHex('boot.dt.address', command='dt_address') assert list(c.output()) == [] c._value = 0x100 assert list(c.output()) == ['dt_address=0x100'] def test_bool_command_init(): c = CommandBool('boot.test.enabled', command='test_mode') assert c.commands == ('test_mode',) assert c.index == 0 assert c.default is False assert c.hint is None assert not c.value c._value = c.update(UserStr('on')) assert c.value def test_bool_command_extract(): c = CommandBool('boot.test.enabled', command='test_mode') config = [ BootCommand('config.txt', 1, cond_all, 'test_mode', '1', hdmi=0), BootCommand('config.txt', 1, cond_all, 'test_mode', 'foo', hdmi=0), ] with warnings.catch_warnings(record=True) as w: assert list(c.extract(config)) == [ (config[0], True), (config[1], None), ] assert len(w) == 1 assert issubclass(w[0].category, ParseWarning) def test_bool_command_output(): c = CommandBool('boot.test.enabled', command='test_mode') assert list(c.output()) == [] c._value = True assert list(c.output()) == ['test_mode=1'] def test_inv_bool_command_init(): c = CommandBoolInv('video.overscan.enabled', command='disable_overscan', default=True) assert c.commands == ('disable_overscan',) assert c.index == 0 assert c.default is True assert c.hint is None assert c.value c._value = c.update(UserStr('off')) assert not c.value def test_inv_bool_command_extract(): c = CommandBoolInv('video.overscan.enabled', command='disable_overscan', default=True) config = [ BootCommand('config.txt', 1, cond_all, 'disable_overscan', '1', hdmi=0), ] assert list(c.extract(config)) == [ (config[0], False), ] def test_inv_bool_command_output(): c = CommandBoolInv('video.overscan.enabled', command='disable_overscan', default=True) assert list(c.output()) == [] c._value = False assert list(c.output()) == ['disable_overscan=1'] def test_force_ignore_command_extract(): c = CommandForceIgnore('video.hdmi.enabled', force='hdmi_force', ignore='hdmi_ignore') config = [ BootCommand('config.txt', 1, cond_all, 'hdmi_force', '1', hdmi=0), BootCommand('config.txt', 2, cond_all, 'hdmi_ignore', '1', hdmi=0), BootCommand('config.txt', 2, cond_all, 'hdmi_ignore', 'blah', hdmi=0), ] with warnings.catch_warnings(record=True) as w: assert list(c.extract(config)) == [ (config[0], True), (config[1], False), (config[2], False), ] assert len(w) == 1 assert issubclass(w[0].category, ParseWarning) def test_force_ignore_command_output(): c = CommandForceIgnore('video.hdmi.enabled', force='hdmi_force', ignore='hdmi_ignore') assert c._value is None assert list(c.output()) == [] c._value = False assert list(c.output()) == ['hdmi_ignore=1'] c._value = True assert list(c.output()) == ['hdmi_force=1'] c = CommandForceIgnore('video.hdmi.enabled', force='hdmi_force', ignore='hdmi_ignore', index=1) c._value = False assert list(c.output()) == ['hdmi_ignore:1=1'] c._value = True assert list(c.output()) == ['hdmi_force:1=1'] def test_mask_command_init(): cm = CommandMaskMaster('video.dpi.format', command='dpi_format', mask=0xf, dummies={'.clock'}) cd = CommandMaskDummy('video.dpi.clock', command='dpi_format', mask=0x10) assert cm.commands == cd.commands == ('dpi_format',) assert cm.index == cd.index == 0 assert cm.default == cd.default == 0 assert cm._mask == 0xf assert cm._shift == 0 assert not cm._bool assert cm._names == ('video.dpi.format', 'video.dpi.clock') assert cd._mask == 0x10 assert cd._shift == 4 assert cd._bool assert cm.value == 0 cm._value = cm.update(UserStr('0x8')) assert cm.value == 8 assert cd.value == 0 cd._value = cd.update(UserStr('on')) assert cd.value == 1 def test_mask_command_extract(): cm = CommandMaskMaster('video.dpi.format', command='dpi_format', mask=0xf, dummies={'.clock'}) cd = CommandMaskDummy('video.dpi.clock', command='dpi_format', mask=0x10) config = [ BootCommand('config.txt', 1, cond_all, 'dpi_format', '0x18', hdmi=0), ] assert list(cm.extract(config)) == [ (config[0], 8), ] assert list(cd.extract(config)) == [ (config[0], True), ] def test_mask_command_output(): settings = make_settings( CommandMaskMaster('video.dpi.format', command='dpi_format', mask=0xf, dummies={'.clock'}), CommandMaskDummy('video.dpi.clock', command='dpi_format', mask=0x10)) cm = settings['video.dpi.format'] cd = settings['video.dpi.clock'] assert list(cm.output()) == [] assert list(cd.output()) == [] cm._value = 8 assert list(cm.output()) == ['dpi_format=0x8'] assert list(cd.output()) == [] cd._value = True assert list(cm.output()) == ['dpi_format=0x18'] with pytest.raises(DelegatedOutput): list(cd.output()) def test_filename_command_hint(): settings = make_settings( Command('boot.prefix', command='os_prefix', default=''), CommandFilename('fw.filename', command='start_file', default='start.elf')) prefix = settings['boot.prefix'] start = settings['fw.filename'] assert prefix.value == '' assert start.value == 'start.elf' assert start.filename == start.value assert start.hint is None prefix._value = 'boot1/' assert start.filename == 'boot1/start.elf' assert start.hint == "'boot1/start.elf' with boot.prefix" def test_display_mode_hint(): settings = make_settings( CommandDisplayGroup('video.hdmi.group', command='hdmi_group'), CommandDisplayMode('video.hdmi.mode', command='hdmi_mode')) group = settings['video.hdmi.group'] mode = settings['video.hdmi.mode'] assert group.hint == 'auto from EDID' assert mode.hint == 'auto from EDID' group._value = 1 group.validate() with pytest.raises(ValueError): mode.validate() mode._value = 4 mode.validate() assert group.hint == 'CEA' assert mode.hint == '720p @60Hz' mode._value = 94 mode.validate() assert group.hint == 'CEA' assert mode.hint == '2160p @25Hz (Pi 4)' group._value = 2 mode._value = 87 mode.validate() assert group.hint == 'DMT' assert mode.hint == 'user timings' def test_display_timings_extract(): t = CommandDisplayTimings('video.timings', command='video_timings') config = [ BootCommand('config.txt', 1, cond_all, 'video_timings', '', hdmi=0), BootCommand('config.txt', 2, cond_all, 'video_timings', ' '.join(['0'] * 17), hdmi=0), BootCommand('config.txt', 3, cond_all, 'video_timings', '0 1 0 0 foo 1 2 3 4 5', hdmi=0), ] with warnings.catch_warnings(record=True) as w: assert list(t.extract(config)) == [ (config[0], []), (config[1], [0] * 17), (config[2], []), ] assert len(w) == 1 assert issubclass(w[0].category, ParseWarning) def test_display_timings_update(): t = CommandDisplayTimings('video.timings', command='video_timings') assert not t.modified assert t.value == [] t._value = t.update(UserStr(','.join(['0'] * 17))) t.validate() assert t.modified assert t.value == [0] * 17 t._value = t.update(UserStr(','.join(['15'] * 17))) t.validate() assert t.value == [15] * 17 t._value = t.update(list(range(17))) assert t.value == list(range(17)) t._value = t.update(UserStr('')) t.validate() assert not t.modified assert t.value == [] t._value = [1] * 16 with pytest.raises(ValueError): t.validate() def test_display_timings_output(): t = CommandDisplayTimings('video.timings', command='video_timings') assert list(t.output()) == [] t._value = [0] * 17 assert list(t.output()) == ['video_timings=' + ' '.join(['0'] * 17)] t._value = list(range(17)) assert list(t.output()) == ['video_timings=' + ' '.join(str(i) for i in range(17))] def test_rotate_flip_extract(): settings = make_settings( CommandDisplayRotate('video.rotate', command='hdmi_rotate'), CommandDisplayFlip('video.flip', command='hdmi_rotate')) rot = settings['video.rotate'] flip = settings['video.flip'] config = [ BootCommand('config.txt', 1, cond_all, 'hdmi_rotate', '0x10001', hdmi=0), ] assert list(rot.extract(config)) == [ (config[0], 90), ] assert list(flip.extract(config)) == [ (config[0], 1), ] def test_rotate_flip_update(): settings = make_settings( CommandDisplayRotate('video.rotate', command='hdmi_rotate'), CommandDisplayFlip('video.flip', command='hdmi_rotate')) rot = settings['video.rotate'] flip = settings['video.flip'] rot._value = rot.update(UserStr('90')) assert rot.modified rot.validate() rot._value = rot.update(45) assert rot.modified with pytest.raises(ValueError): rot.validate() rot._value = rot.update(UserStr('')) assert not rot.modified rot.validate() def test_rotate_flip_output(): settings = make_settings( CommandDisplayRotate('video.rotate', command='hdmi_rotate'), CommandDisplayFlip('video.flip', command='hdmi_rotate')) rot = settings['video.rotate'] flip = settings['video.flip'] assert list(rot.output()) == [] assert list(flip.output()) == [] rot._value = 90 assert list(rot.output()) == ['hdmi_rotate=0x1'] assert list(flip.output()) == [] flip._value = 1 assert list(rot.output()) == ['hdmi_rotate=0x10001'] with pytest.raises(DelegatedOutput) as exc: list(flip.output()) assert exc.value.master == 'video.rotate' def test_rotate_flip_indexed_output(): settings = make_settings( CommandDisplayRotate('video.rotate', index=1, command='hdmi_rotate'), CommandDisplayFlip('video.flip', index=1, command='hdmi_rotate')) rot = settings['video.rotate'] flip = settings['video.flip'] assert list(rot.output()) == [] assert list(flip.output()) == [] rot._value = 90 assert list(rot.output()) == ['hdmi_rotate:1=0x1'] assert list(flip.output()) == [] flip._value = 1 assert list(rot.output()) == ['hdmi_rotate:1=0x10001'] with pytest.raises(DelegatedOutput) as exc: list(flip.output()) assert exc.value.master == 'video.rotate' def test_rotate_flip_lcd_output(): settings = make_settings( CommandDisplayRotate('video.rotate', commands=('hdmi_rotate', 'lcd_rotate')), CommandDisplayFlip('video.flip', commands=('hdmi_rotate', 'lcd_rotate'))) rot = settings['video.rotate'] flip = settings['video.flip'] assert list(rot.output()) == [] assert list(flip.output()) == [] rot._value = 90 assert list(rot.output()) == ['lcd_rotate=1'] assert list(flip.output()) == [] flip._value = 1 assert list(rot.output()) == ['hdmi_rotate=0x10001'] with pytest.raises(DelegatedOutput) as exc: list(flip.output()) assert exc.value.master == 'video.rotate' def test_dpi_output(): settings = make_settings( CommandBool('dpi.enabled', command='dpi_enabled'), CommandDPIOutput('dpi.format', command='dpi_format', mask=0xf, dummies={ '.color'}), CommandDPIDummy('dpi.color', command='dpi_format', mask=0x10)) enable = settings['dpi.enabled'] fmt = settings['dpi.format'] col = settings['dpi.color'] assert list(chain(enable.output(), fmt.output(), col.output())) == [] fmt._value = 8 col._value = True assert list(chain(enable.output(), fmt.output())) == [] with pytest.raises(DelegatedOutput) as exc: list(col.output()) assert exc.value.master == 'video.dpi.format' enable._value = True assert list(chain(enable.output(), fmt.output())) == [ 'dpi_enabled=1', 'dpi_format=0x18', ] def test_hdmi_boost_validate(): boost = CommandHDMIBoost('video.boost', command='hdmi_boost', default=5) assert boost.value == 5 assert not boost.modified boost.validate() boost._value = 1 assert boost.value == 1 assert boost.modified boost.validate() boost._value = 12 assert boost.modified with pytest.raises(ValueError): boost.validate() def test_edid_ignore(): ignore = CommandEDIDIgnore('video.edid.ignore', command='hdmi_ignore') assert ignore.value is False assert not ignore.modified assert ignore.hint is None assert list(ignore.output()) == [] ignore._value = ignore.update(UserStr('on')) assert ignore.value is True assert ignore.modified assert ignore.hint is None assert list(ignore.output()) == ['hdmi_ignore=0xa5000080'] config = [BootCommand('config.txt', 1, cond_all, 'hdmi_ignore', '0', hdmi=0)] assert list(ignore.extract(config)) == [(config[0], False)] config = [BootCommand('config.txt', 1, cond_all, 'hdmi_ignore', '0xa5000080', hdmi=0)] assert list(ignore.extract(config)) == [(config[0], True)] def test_boot_delay2(): delay = CommandBootDelay2( 'boot.delay', commands=('boot_delay', 'boot_delay_ms'), default=0) assert delay.value == 0.0 assert not delay.modified delay.validate() config = [ BootCommand('config.txt', 1, cond_all, 'boot_delay', '1', hdmi=0), BootCommand('config.txt', 2, cond_all, 'boot_delay_ms', '500', hdmi=0), BootCommand('config.txt', 3, cond_all, 'boot_delay', '2', hdmi=0), BootCommand('config.txt', 4, cond_all, 'boot_delay', 'foo', hdmi=0), BootCommand('config.txt', 5, cond_all, 'boot_delay_ms', 'bar', hdmi=0), ] with warnings.catch_warnings(record=True) as w: assert list(delay.extract(config)) == [ (config[0], 1.0), (config[1], 1.5), (config[2], 2.5), (config[3], 0.5), (config[4], 0.0), ] assert len(w) == 2 assert issubclass(w[0].category, ParseWarning) assert issubclass(w[1].category, ParseWarning) assert list(delay.output()) == [] delay._value = delay.update(UserStr('2.5')) assert delay.modified delay.validate() assert list(delay.output()) == ['boot_delay=2', 'boot_delay_ms=500'] delay._value = delay.update(UserStr('0.5')) delay.validate() assert list(delay.output()) == ['boot_delay_ms=500'] delay._value = delay.update(UserStr('0.0')) delay.validate() assert list(delay.output()) == [] delay._value = -1 with pytest.raises(ValueError): delay.validate() def test_kernel_address(): settings = make_settings( CommandKernel64('kernel.64bit', commands=('arm_64bit', 'arm_control')), CommandKernelAddress('kernel.addr', commands=('kernel_address', 'kernel_old'))) arm8 = settings['kernel.64bit'] addr = settings['kernel.addr'] assert not arm8.value assert not addr.modified assert addr.value == 0x8000 arm8._value = True assert not addr.modified assert addr.value == 0x80000 config = [ BootCommand('config.txt', 1, cond_all, 'arm_64bit', '0', hdmi=0), BootCommand('config.txt', 2, cond_all, 'arm_64bit', 'erm', hdmi=0), BootCommand('config.txt', 3, cond_all, 'arm_control', '0x202', hdmi=0), BootCommand('config.txt', 4, cond_all, 'kernel_address', 'f00', hdmi=0), BootCommand('config.txt', 5, cond_all, 'kernel_address', '0xf00', hdmi=0), BootCommand('config.txt', 6, cond_all, 'kernel_old', '0', hdmi=0), BootCommand('config.txt', 7, cond_all, 'kernel_old', '1', hdmi=0), ] with warnings.catch_warnings(record=True) as w: assert list(arm8.extract(config)) == [ (config[0], False), (config[1], None), (config[2], True), ] assert list(addr.extract(config)) == [ (config[3], None), (config[4], 0xf00), (config[6], 0), ] assert len(w) == 2 assert issubclass(w[0].category, ParseWarning) assert issubclass(w[1].category, ParseWarning) def test_kernel_filename(): with mock.patch('pibootctl.setting.get_board_type') as get_board_type: settings = make_settings( CommandKernel64('kernel.64bit', commands=('arm_64bit', 'arm_control')), CommandKernelFilename('kernel.filename', command='kernel_filename')) arm8 = settings['kernel.64bit'] filename = settings['kernel.filename'] assert not filename.modified get_board_type.return_value = 'pi0' assert filename.value == 'kernel.img' get_board_type.return_value = 'pi4' assert filename.value == 'kernel7l.img' get_board_type.return_value = 'pi3+' assert filename.value == 'kernel7.img' arm8._value = True assert filename.value == 'kernel8.img' def test_camera_firmware(fw_settings): with mock.patch('pibootctl.setting.get_board_type') as get_board_type: cam = fw_settings['camera.enabled'] mem = fw_settings['gpu.mem'] start = fw_settings['boot.firmware.filename'] fixup = fw_settings['boot.firmware.fixup'] assert not cam.modified assert not cam.value cam.validate() # camera mode can be enabled (by default) by specifying firmware manually get_board_type.return_value = 'pi0' start._value = 'start_x.elf' fixup._value = 'fixup_x.dat' assert not cam.modified assert cam.value assert cam.default cam.validate() assert list(cam.output()) == [] get_board_type.return_value = 'pi4' assert not cam.value assert not cam.default start._value = None fixup._value = None cam._value = True cam.validate() assert cam.modified assert cam.value assert start.value == 'start4x.elf' assert fixup.value == 'fixup4x.dat' assert list(cam.output()) == ['start_x=1'] mem._value = 32 get_board_type.return_value = 'pi0' with pytest.raises(ValueError): cam.validate() def test_camera_firmware_extract(fw_settings): cam = fw_settings['camera.enabled'] config = [ BootCommand('config.txt', 1, cond_all, 'gpu_mem', '192', hdmi=0), BootCommand('config.txt', 2, cond_all, 'start_x', '1', hdmi=0), BootCommand('config.txt', 3, cond_all, 'start_x', '0', hdmi=0), BootCommand('config.txt', 4, cond_all, 'start_debug', '1', hdmi=0), BootCommand('syscfg.txt', 1, cond_all, 'start_debug', '1', hdmi=0), ] assert list(cam.extract(config)) == [ (config[1], True), (config[2], None), ] def test_firmware_file_extract(fw_settings): start = fw_settings['boot.firmware.filename'] fixup = fw_settings['boot.firmware.fixup'] config = [ BootCommand('config.txt', 1, cond_all, 'start_file', 'start.elf', hdmi=0), BootCommand('config.txt', 2, cond_all, 'fixup_file', 'fixup.dat', hdmi=0), BootCommand('usercfg.txt', 1, cond_all, 'start_file', 'start_x.elf', hdmi=0), BootCommand('usercfg.txt', 2, cond_all, 'fixup_file', 'fixup_x.dat', hdmi=0), ] assert list(start.extract(config)) == [(config[0], 'start.elf')] assert list(fixup.extract(config)) == [(config[1], 'fixup.dat')] def test_debug_firmware(fw_settings): with mock.patch('pibootctl.setting.get_board_type') as get_board_type: debug = fw_settings['boot.debug.enabled'] start = fw_settings['boot.firmware.filename'] fixup = fw_settings['boot.firmware.fixup'] assert not debug.modified assert not debug.value debug.validate() # debug mode can be enabled by specifying firmware manually get_board_type.return_value = 'pi0' start._value = 'start_db.elf' fixup._value = 'fixup_db.dat' assert not debug.modified assert debug.value assert debug.default debug.validate() assert list(debug.output()) == [] get_board_type.return_value = 'pi4' assert not debug.value assert not debug.default start._value = None fixup._value = None debug._value = True debug.validate() assert debug.modified assert debug.value assert start.value == 'start4db.elf' assert fixup.value == 'fixup4db.dat' assert list(debug.output()) == ['start_debug=1'] def test_debug_firmware_extract(fw_settings): debug = fw_settings['boot.debug.enabled'] config = [ BootCommand('config.txt', 1, cond_all, 'start_debug', '1', hdmi=0), BootCommand('config.txt', 2, cond_all, 'start_debug', '0', hdmi=0), BootCommand('syscfg.txt', 1, cond_all, 'start_debug', '1', hdmi=0), ] assert list(debug.extract(config)) == [ (config[0], True), (config[1], None), ] def test_firmware_filename(fw_settings): with mock.patch('pibootctl.setting.get_board_type') as get_board_type: cam = fw_settings['camera.enabled'] debug = fw_settings['boot.debug.enabled'] start = fw_settings['boot.firmware.filename'] fixup = fw_settings['boot.firmware.fixup'] mem = fw_settings['gpu.mem'] get_board_type.return_value = 'pi0' assert not start.modified assert not fixup.modified assert start.value == 'start.elf' assert fixup.value == 'fixup.dat' cam._value = True assert start.value == 'start_x.elf' assert fixup.value == 'fixup_x.dat' debug._value = True assert start.value == 'start_db.elf' assert fixup.value == 'fixup_db.dat' mem._value = 16 assert start.value == 'start_cd.elf' assert fixup.value == 'fixup_cd.dat' mem._value = None cam._value = None debug._value = None get_board_type.return_value = 'pi4' assert not start.modified assert not fixup.modified assert start.value == 'start4.elf' assert fixup.value == 'fixup4.dat' cam._value = True assert start.value == 'start4x.elf' assert fixup.value == 'fixup4x.dat' debug._value = True assert start.value == 'start4db.elf' assert fixup.value == 'fixup4db.dat' mem._value = 16 assert start.value == 'start4cd.elf' assert fixup.value == 'fixup4cd.dat' def test_dt_addr(): dt_addr = CommandDeviceTreeAddress('dt.addr', command='device_tree_address') assert not dt_addr.modified assert dt_addr.value == 0 assert dt_addr.hint == 'auto' dt_addr._value = 0x3000000 assert dt_addr.modified assert dt_addr.value == 0x3000000 assert dt_addr.hint == '0x3000000' def test_initrd_addr(): initrd_addr = CommandRamFSAddress('initrd.addr', commands=('ramfsaddr', 'initramfs')) assert not initrd_addr.modified assert initrd_addr.value == 0 assert initrd_addr.hint == 'auto' initrd_addr._value = 0x2400000 assert initrd_addr.modified assert initrd_addr.value == 0x2400000 assert initrd_addr.hint == '0x2400000' def test_initrd_addr_extract(): initrd_addr = CommandRamFSAddress('initrd.addr', commands=('ramfsaddr', 'initramfs')) config = [ BootCommand('config.txt', 1, cond_all, 'initramfs', ('initrd.img', 'followkernel'), hdmi=0), BootCommand('config.txt', 2, cond_all, 'initramfs', ('initrd.img', '0x2400000'), hdmi=0), BootCommand('config.txt', 3, cond_all, 'ramfsaddr', '0x2700000', hdmi=0), BootCommand('config.txt', 4, cond_all, 'ramfsaddr', 'f000000', hdmi=0) ] with warnings.catch_warnings(record=True) as w: assert list(initrd_addr.extract(config)) == [ (config[0], None), (config[1], 0x2400000), (config[2], 0x2700000), (config[3], None), ] assert len(w) == 1 assert issubclass(w[0].category, ParseWarning) def test_initrd_filename(): settings = make_settings( Command('boot.prefix', command='os_prefix', default=''), CommandRamFSFilename('initrd.file', commands=('ramfsfile', 'initramfs'))) prefix = settings['boot.prefix'] initrd = settings['initrd.file'] initrd.validate() assert not initrd.modified assert initrd.value == [] assert initrd.filename == [] assert initrd.hint is None initrd._value = initrd.update(['initrd.img']) initrd.validate() assert initrd.modified assert initrd.value == ['initrd.img'] assert initrd.filename == ['initrd.img'] assert initrd.hint is None initrd._value = initrd.update(UserStr(' initrd.img,splash.img')) initrd.validate() assert initrd.modified assert initrd.value == ['initrd.img', 'splash.img'] assert initrd.filename == ['initrd.img', 'splash.img'] assert initrd.hint is None prefix._value = 'boot/' assert initrd.modified assert initrd.value == ['initrd.img', 'splash.img'] assert initrd.filename == ['boot/initrd.img', 'boot/splash.img'] assert initrd.hint == "['boot/initrd.img', 'boot/splash.img'] with boot.prefix" initrd._value = initrd.update(['initrd{}.img'.format(i) for i in range(10)]) with pytest.raises(ValueError): initrd.validate() def test_initrd_filename_extract(): initrd = CommandRamFSFilename('initrd.file', commands=('ramfsfile', 'initramfs')) config = [ BootCommand('config.txt', 1, cond_all, 'initramfs', ('initrd.img', 'followkernel'), hdmi=0), BootCommand('config.txt', 2, cond_all, 'initramfs', ('initrd.img,splash.img', '0x2400000'), hdmi=0), BootCommand('config.txt', 3, cond_all, 'ramfsfile', 'initrd.img,net.img', hdmi=0), ] assert list(initrd.extract(config)) == [ (config[0], ['initrd.img']), (config[1], ['initrd.img', 'splash.img']), (config[2], ['initrd.img', 'net.img']), ] def test_initrd_filename_output(): settings = make_settings( CommandRamFSFilename('initrd.filename', commands=('ramfsfile', 'initramfs')), CommandRamFSAddress('initrd.address', commands=('ramfsaddr', 'initramfs')), ) initfn = settings['initrd.filename'] initaddr = settings['initrd.address'] assert list(initfn.output()) == [] with pytest.raises(DelegatedOutput) as exc: list(initaddr.output()) assert exc.value.master == 'initrd.filename' initfn._value = ['initrd.img'] assert list(initfn.output()) == ['initramfs initrd.img followkernel'] with pytest.raises(DelegatedOutput) as exc: list(initaddr.output()) assert exc.value.master == 'initrd.filename' initfn._value = ['initrd.img', 'splash.img'] assert list(initfn.output()) == ['initramfs initrd.img,splash.img followkernel'] with pytest.raises(DelegatedOutput) as exc: list(initaddr.output()) assert exc.value.master == 'initrd.filename' initaddr._value = 0x2400000 assert list(chain(initfn.output(), initaddr.output())) == [ 'ramfsfile=initrd.img,splash.img', 'ramfsaddr=0x2400000'] def test_serial_bt(): with mock.patch('pibootctl.setting.get_board_type') as get_board_type: settings = make_settings( CommandSerialEnabled('serial.enabled', command='enable_uart'), OverlayBluetoothEnabled('bluetooth.enabled'), OverlaySerialUART('serial.uart')) enable = settings['serial.enabled'] bt = settings['bluetooth.enabled'] uart = settings['serial.uart'] get_board_type.return_value = None enable.validate() uart.validate() assert not enable.modified assert not uart.modified assert enable.value assert enable.default assert uart.value == 0 assert uart.default == 0 assert uart.hint == '/dev/ttyAMA0; PL011' # Pi3 (and above) moves serial to mini-UART get_board_type.return_value = 'pi3' enable.validate() uart.validate() assert not enable.value assert not enable.default assert uart.value == 1 assert uart.default == 1 assert uart.hint == '/dev/ttyS0; mini-UART' # ... but can be forced back to PL011 enable._value = enable.update(UserStr('on')) uart._value = uart.update(UserStr('0')) enable.validate() uart.validate() assert enable.modified assert enable.value assert not enable.default assert uart.modified assert uart.value == 0 assert uart.default == 1 # ... and will default to that if Bluetooth is disabled uart._value = None bt._value = bt.update(UserStr('off')) enable.validate() uart.validate() assert enable.value assert enable.default assert uart.value == 0 assert uart.default == 0 # Can't use mini-UART with BT disabled (because what would be the point) uart._value = 1 with pytest.raises(ValueError): uart.validate() def test_serial_bt_extract(): settings = make_settings( CommandSerialEnabled('serial.enabled', command='enable_uart'), OverlayBluetoothEnabled('bluetooth.enabled'), OverlaySerialUART('serial.uart')) enable = settings['serial.enabled'] bt = settings['bluetooth.enabled'] uart = settings['serial.uart'] config = [ BootCommand('config.txt', 1, cond_all, 'enable_uart', '1', hdmi=0), BootOverlay('config.txt', 2, cond_all, 'pi3-miniuart-bt'), BootOverlay('config.txt', 3, cond_all, 'disable-bt'), BootOverlay('config.txt', 4, cond_all, 'foo'), ] assert list(enable.extract(config)) == [(config[0], True)] assert list(bt.extract(config)) == [(config[1], True), (config[2], False)] assert list(uart.extract(config)) == [(config[1], 0)] def test_serial_bt_output(): with mock.patch('pibootctl.setting.get_board_type') as get_board_type: settings = make_settings( CommandSerialEnabled('serial.enabled', command='enable_uart'), OverlayBluetoothEnabled('bluetooth.enabled'), OverlaySerialUART('serial.uart')) enable = settings['serial.enabled'] bt = settings['bluetooth.enabled'] uart = settings['serial.uart'] get_board_type.return_value = 'pi3' assert list(chain(enable.output(), bt.output(), uart.output())) == [] bt._value = True assert list(chain(enable.output(), bt.output(), uart.output())) == [] enable._value = True assert list(chain(enable.output(), bt.output(), uart.output())) == ['enable_uart=1'] uart._value = 0 assert list(chain(enable.output(), bt.output())) == [ 'enable_uart=1', 'dtoverlay=miniuart-bt', ] with pytest.raises(DelegatedOutput) as exc: list(uart.output()) assert exc.value.master == 'bluetooth.enabled' bt._value = False assert list(chain(enable.output(), bt.output())) == [ 'enable_uart=1', 'dtoverlay=disable-bt', ] with pytest.raises(DelegatedOutput) as exc: list(uart.output()) assert exc.value.master == 'bluetooth.enabled' def test_l2_cache(): with mock.patch('pibootctl.setting.get_board_type') as get_board_type: cache = CommandCPUL2Cache('l2.enabled', command='disable_l2_cache') get_board_type.return_value = None assert not cache.modified assert cache.default is None get_board_type.return_value = 'pi0' assert cache.default is True get_board_type.return_value = 'pi4' assert cache.default is False def test_cpu_freq(): with mock.patch('pibootctl.setting.get_board_type') as get_board_type: settings = make_settings( CommandCPUFreqMax('cpu.max', command='arm_freq'), CommandCPUFreqMin('cpu.min', command='arm_freq_min'), CommandBool('cpu.turbo.force', command='force_turbo')) cpu_max = settings['cpu.max'] cpu_min = settings['cpu.min'] turbo = settings['cpu.turbo.force'] get_board_type.return_value = None assert not turbo.modified assert not turbo.value assert not cpu_min.modified assert not cpu_max.modified assert cpu_min.hint == cpu_max.hint == 'MHz' assert (cpu_min.default, cpu_max.default) == (0, 0) get_board_type.return_value = 'pi0' assert (cpu_min.default, cpu_max.default) == (700, 1000) get_board_type.return_value = 'pi1' assert (cpu_min.default, cpu_max.default) == (700, 700) get_board_type.return_value = 'pi2' assert (cpu_min.default, cpu_max.default) == (600, 900) get_board_type.return_value = 'pi3' assert (cpu_min.default, cpu_max.default) == (600, 1200) get_board_type.return_value = 'pi3+' assert (cpu_min.default, cpu_max.default) == (600, 1400) get_board_type.return_value = 'pi4' assert (cpu_min.default, cpu_max.default) == (600, 1500) cpu_min.validate() cpu_max.validate() # Turbo locks min to max turbo._value = True assert (cpu_min.default, cpu_max.default) == (1500, 1500) cpu_min.validate() cpu_max.validate() # ... on all models get_board_type.return_value = 'pi3' assert (cpu_min.default, cpu_max.default) == (1200, 1200) cpu_min.validate() cpu_max.validate() # max can't be less than min turbo._value = False cpu_max._value = 600 cpu_min._value = 1200 with pytest.raises(ValueError): cpu_max.validate() def test_gpu_freq(): with mock.patch('pibootctl.setting.get_board_type') as get_board_type: # There are an absolute ton of cross-dependencies on GPU frequencies: # between the different blocks, the enabled video-outs (on the 4), and # whether or not mini-UART serial is enabled (which in turn depends on # Bluetooth) ... urgh! settings = make_settings( CommandCoreFreqMax('gpu.core.frequency.max', commands=('core_freq', 'gpu_freq')), CommandCoreFreqMin('gpu.core.frequency.min', commands=('core_freq_min', 'gpu_freq_min')), CommandGPUFreqMax('gpu.h264.frequency.max', commands=('h264_freq', 'gpu_freq')), CommandGPUFreqMin('gpu.h264.frequency.min', commands=('h264_freq_min', 'gpu_freq_min')), CommandGPUFreqMax('gpu.isp.frequency.max', commands=('isp_freq', 'gpu_freq')), CommandGPUFreqMin('gpu.isp.frequency.min', commands=('isp_freq_min', 'gpu_freq_min')), CommandGPUFreqMax('gpu.v3d.frequency.max', commands=('v3d_freq', 'gpu_freq')), CommandGPUFreqMin('gpu.v3d.frequency.min', commands=('v3d_freq_min', 'gpu_freq_min')), CommandBool('cpu.turbo.force', command='force_turbo'), CommandBool('video.hdmi.4kp60', command='hdmi_enable_4kp60'), CommandTVOut('video.tv.enabled', command='enable_tvout'), OverlayBluetoothEnabled('bluetooth.enabled'), OverlaySerialUART('serial.uart'), CommandSerialEnabled('serial.enabled', command='enable_uart')) get_board_type.return_value = None assert settings['serial.enabled'].value assert settings['serial.uart'].value == 0 assert not settings['video.tv.enabled'].modified assert settings['video.tv.enabled'].value assert not settings['video.hdmi.4kp60'].modified assert not settings['cpu.turbo.force'].value assert not settings['gpu.core.frequency.min'].modified assert not settings['gpu.core.frequency.max'].modified for setting in settings.values(): if 'frequency' in setting.name: assert setting.hint == 'MHz' assert setting.default == 0 get_board_type.return_value = 'pi0w' assert ( settings['gpu.core.frequency.min'].value, settings['gpu.core.frequency.max'].value, ) == (250, 400) assert ( settings['gpu.h264.frequency.min'].value, settings['gpu.h264.frequency.max'].value, ) == (250, 300) get_board_type.return_value = 'pi2' assert ( settings['gpu.core.frequency.min'].value, settings['gpu.core.frequency.max'].value, ) == (250, 250) assert ( settings['gpu.h264.frequency.min'].value, settings['gpu.h264.frequency.max'].value, ) == (250, 250) get_board_type.return_value = 'pi4' assert not settings['video.tv.enabled'].modified assert not settings['video.tv.enabled'].value assert ( settings['gpu.core.frequency.min'].value, settings['gpu.core.frequency.max'].value, ) == (250, 500) assert ( settings['gpu.h264.frequency.min'].value, settings['gpu.h264.frequency.max'].value, ) == (500, 500) # Turbo locks min to max settings['cpu.turbo.force']._value = True assert ( settings['gpu.core.frequency.min'].value, settings['gpu.core.frequency.max'].value, ) == (500, 500) settings['gpu.h264.frequency.max']._value = 600 assert ( settings['gpu.h264.frequency.min'].value, settings['gpu.h264.frequency.max'].value, ) == (600, 600) settings['gpu.h264.frequency.max']._value = None # Serial over mini-UART locks core frequency settings['cpu.turbo.force']._value = None settings['serial.enabled']._value = True assert settings['serial.uart'].value == 1 assert ( settings['gpu.core.frequency.min'].default, settings['gpu.core.frequency.max'].default, ) == (250, 250) # TV lowers max settings['serial.enabled']._value = None settings['video.tv.enabled']._value = True settings['video.tv.enabled'].validate() settings['gpu.core.frequency.max'].validate() assert ( settings['gpu.core.frequency.min'].default, settings['gpu.core.frequency.max'].default, ) == (250, 360) # Can't have TV and HDMI4kp60 settings['video.hdmi.4kp60']._value = True with pytest.raises(ValueError): settings['video.tv.enabled'].validate() # 4kp60 raises min and max settings['video.tv.enabled']._value = False settings['video.tv.enabled'].validate() settings['gpu.core.frequency.max'].validate() assert ( settings['gpu.core.frequency.min'].default, settings['gpu.core.frequency.max'].default, ) == (275, 550) # max can't be less than min settings['gpu.core.frequency.max']._value = 250 with pytest.raises(ValueError): settings['gpu.core.frequency.max'].validate() settings['gpu.h264.frequency.max']._value = 250 with pytest.raises(ValueError): settings['gpu.h264.frequency.max'].validate() def test_gpu_freq_output(): with mock.patch('pibootctl.setting.get_board_type') as get_board_type: settings = make_settings( CommandCoreFreqMax('gpu.core.frequency.max', commands=('core_freq', 'gpu_freq')), CommandCoreFreqMin('gpu.core.frequency.min', commands=('core_freq_min', 'gpu_freq_min')), CommandGPUFreqMax('gpu.h264.frequency.max', commands=('h264_freq', 'gpu_freq')), CommandGPUFreqMin('gpu.h264.frequency.min', commands=('h264_freq_min', 'gpu_freq_min')), CommandGPUFreqMax('gpu.isp.frequency.max', commands=('isp_freq', 'gpu_freq')), CommandGPUFreqMin('gpu.isp.frequency.min', commands=('isp_freq_min', 'gpu_freq_min')), CommandGPUFreqMax('gpu.v3d.frequency.max', commands=('v3d_freq', 'gpu_freq')), CommandGPUFreqMin('gpu.v3d.frequency.min', commands=('v3d_freq_min', 'gpu_freq_min')), CommandBool('cpu.turbo.force', command='force_turbo'), OverlayBluetoothEnabled('bluetooth.enabled'), OverlaySerialUART('serial.uart'), CommandSerialEnabled('serial.enabled', command='enable_uart')) def get_gpu_output(): lines = [] errors = [] for setting in sorted(settings.values(), key=attrgetter('key')): if setting.name.startswith('gpu.'): try: lines.extend(setting.output()) except DelegatedOutput as exc: errors.append(exc) return lines, errors get_board_type.return_value = None lines, errors = get_gpu_output() assert lines == [] assert errors == [] get_board_type.return_value = 'pi3' settings['gpu.h264.frequency.max']._value = 600 lines, errors = get_gpu_output() assert lines == ['h264_freq=600'] assert errors == [] settings['gpu.core.frequency.max']._value = 600 lines, errors = get_gpu_output() assert lines == ['core_freq=600', 'h264_freq=600'] assert errors == [] settings['gpu.isp.frequency.max']._value = 600 settings['gpu.v3d.frequency.max']._value = 600 lines, errors = get_gpu_output() assert lines == ['gpu_freq=600'] assert len(errors) == 3 assert all(isinstance(exc, DelegatedOutput) for exc in errors) assert {exc.master for exc in errors} == {'gpu.core.frequency.max'} settings['gpu.core.frequency.min']._value = 400 settings['gpu.h264.frequency.min']._value = 400 lines, errors = get_gpu_output() assert lines == ['gpu_freq=600', 'core_freq_min=400', 'h264_freq_min=400'] assert len(errors) == 3 assert all(isinstance(exc, DelegatedOutput) for exc in errors) assert {exc.master for exc in errors} == {'gpu.core.frequency.max'} settings['gpu.isp.frequency.min']._value = 400 settings['gpu.v3d.frequency.min']._value = 400 lines, errors = get_gpu_output() assert lines == ['gpu_freq=600', 'gpu_freq_min=400'] assert len(errors) == 6 assert all(isinstance(exc, DelegatedOutput) for exc in errors) assert {exc.master for exc in errors} == { 'gpu.core.frequency.max', 'gpu.core.frequency.min'} def test_gpu_mem(): with mock.patch('pibootctl.setting.get_board_mem') as get_board_mem: mem = CommandGPUMem( 'gpu.mem', default=64, commands=('gpu_mem', 'gpu_mem_256', 'gpu_mem_512', 'gpu_mem_1024')) assert mem.hint == 'Mb' config = [ BootCommand('config.txt', 1, cond_all, 'gpu_mem_1024', '256', hdmi=0), BootCommand('config.txt', 2, cond_all, 'gpu_mem_512', '192', hdmi=0), BootCommand('config.txt', 3, cond_all, 'gpu_mem', '96', hdmi=0), BootCommand('config.txt', 4, cond_all, 'gpu_mem_256', '64', hdmi=0), BootCommand('config.txt', 5, cond_all, 'gpu_mem', 'a0', hdmi=0), ] with warnings.catch_warnings(record=True) as w: get_board_mem.return_value = 1024 assert list(mem.extract(config)) == [ (config[0], 256), (config[2], 256), (config[4], 256), ] assert len(w) == 1 assert issubclass(w[0].category, ParseWarning) mem._value = 8 with pytest.raises(ValueError): mem.validate() with warnings.catch_warnings(record=True) as w: get_board_mem.return_value = 512 assert list(mem.extract(config)) == [ (config[1], 192), (config[2], 192), (config[4], 192), ] assert len(w) == 1 assert issubclass(w[0].category, ParseWarning) mem._value = 256 mem.validate() with warnings.catch_warnings(record=True) as w: get_board_mem.return_value = 256 assert list(mem.extract(config)) == [ (config[2], 96), (config[3], 64), (config[4], 64), ] assert len(w) == 1 assert issubclass(w[0].category, ParseWarning) mem._value = 256 with pytest.raises(ValueError): mem.validate() def test_total_mem(): with mock.patch('pibootctl.setting.get_board_mem') as get_board_mem: mem = CommandTotalMem('total.mem', command='total_mem') assert mem.hint == 'Mb' config = [ BootCommand('config.txt', 1, cond_all, 'total_mem', '256', hdmi=0), BootCommand('syscfg.txt', 1, cond_all, 'total_mem', '512', hdmi=0), ] get_board_mem.return_value = 1024 assert list(mem.extract(config)) == [(config[0], 256)] mem._value = 8 with pytest.raises(ValueError): mem.validate() def test_overlay_dwc2(): with mock.patch('pibootctl.setting.get_board_type') as get_board_type: settings = make_settings(OverlayDWC2('usb.dwc2.enabled')) dwc2 = settings['usb.dwc2.enabled'] get_board_type.return_value = 'pi2' assert not dwc2.default assert not dwc2.value dwc2.validate() get_board_type.return_value = 'pi0w' assert dwc2.default assert dwc2.value dwc2._value = dwc2.update(UserStr('no')) assert not dwc2.value dwc2.validate() def test_overlay_dwc2_extract(): settings = make_settings(OverlayDWC2('usb.dwc2.enabled')) dwc2 = settings['usb.dwc2.enabled'] config = [BootOverlay('config.txt', 1, cond_all, 'miniuart-bt')] assert list(dwc2.extract(config)) == [] config = [BootOverlay('config.txt', 1, cond_all, 'dwc-otg')] assert list(dwc2.extract(config)) == [(config[0], False)] config = [BootOverlay('config.txt', 1, cond_all, 'dwc2')] assert list(dwc2.extract(config)) == [(config[0], True)] def test_overlay_dwc2_output(): settings = make_settings(OverlayDWC2('usb.dwc2.enabled')) dwc2 = settings['usb.dwc2.enabled'] assert list(dwc2.output()) == [] dwc2._value = False assert list(dwc2.output()) == ['dtoverlay=dwc-otg'] dwc2._value = True assert list(dwc2.output()) == ['dtoverlay=dwc2'] def test_overlay_kms(): settings = make_settings(OverlayKMS('video.firmware.mode')) kms = settings['video.firmware.mode'] assert kms.default == 'legacy' assert kms.value == 'legacy' assert kms.hint == 'no KMS' kms.validate() kms._value = kms.update('fkms') assert kms.hint == 'Fake KMS' kms.validate() kms._value = 'blah' with pytest.raises(ValueError): kms.validate() def test_overlay_kms_extract(): settings = make_settings(OverlayKMS('video.firmware.mode')) kms = settings['video.firmware.mode'] config = [BootOverlay('config.txt', 1, cond_all, 'miniuart-bt')] assert list(kms.extract(config)) == [] config = [BootOverlay('config.txt', 1, cond_all, 'vc4-kms-v3d')] assert list(kms.extract(config)) == [(config[0], 'kms')] config = [BootOverlay('config.txt', 1, cond_all, 'vc4-fkms-v3d')] assert list(kms.extract(config)) == [(config[0], 'fkms')] def test_overlay_kms_output(): settings = make_settings(OverlayKMS('video.firmware.mode')) kms = settings['video.firmware.mode'] assert list(kms.output()) == [] kms._value = 'fkms' assert list(kms.output()) == ['dtoverlay=vc4-fkms-v3d'] kms._value = 'kms' assert list(kms.output()) == ['dtoverlay=vc4-kms-v3d'] def test_output_order(): settings = Settings() settings['spi.enabled']._value = True settings['i2c.enabled']._value = True settings['bluetooth.enabled']._value = False def get_output(): lines = [] errors = [] for setting in sorted(settings.values(), key=attrgetter('key')): try: lines.extend(setting.output()) except DelegatedOutput as exc: errors.append(exc) return lines, errors lines, errors = get_output() assert lines == [ # base overlay params must come first 'dtparam=i2c_arm=on', 'dtparam=spi=on', 'dtoverlay=disable-bt', ] assert len(errors) == 1 assert isinstance(errors[0], DelegatedOutput) assert errors[0].master == 'boot.initramfs.filename' def test_video_license(): lic = CommandVideoLicense('video.license.mpg2', command='decode_MPG2') config = [ BootCommand('config.txt', 1, cond_all, 'decode_MPG2', '0x12345678', hdmi=0), ] assert list(lic.extract(config)) == [(config[0], ['0x12345678'])] assert list(lic.output()) == [] lic._value = lic.update(UserStr('1,2,3')) assert lic.value == ['1', '2', '3'] assert list(lic.output()) == ['decode_MPG2=1,2,3'] lic._value = [0] * 10 with pytest.raises(ValueError): lic.validate() def test_gpio_settings(): s = make_settings(*( spec for index in range(28) for spec in ( CommandGPIOMode('gpio{}.mode'.format(index), command='gpio', index=index), CommandGPIOState('gpio{}.state'.format(index), command='gpio', index=index), ) )) config = [ BootCommand('config.txt', 1, cond_all, 'gpio', '2,5-7=op,dh'), BootCommand('config.txt', 2, cond_all, 'gpio', '2,3=ip,op,ip,pu'), BootCommand('config.txt', 3, cond_all, 'gpio', '4,0xf=ip'), # only applies to 4 BootCommand('config.txt', 4, cond_all, 'gpio', '0-7=ip,foo'), # ignored ] with warnings.catch_warnings(record=True) as w: assert list(s['gpio0.mode'].extract(config)) == [] assert list(s['gpio0.state'].extract(config)) == [] assert list(s['gpio2.mode'].extract(config)) == [ (config[0], 'out'), (config[1], 'in'), ] assert list(s['gpio2.state'].extract(config)) == [ (config[0], 'high'), (config[1], 'up'), ] assert len(w) == 2 assert issubclass(w[0].category, ParseWarning) assert issubclass(w[1].category, ParseWarning) for g in (2, 3, 4): s['gpio{}.mode'.format(g)]._value = 'in' s['gpio{}.state'.format(g)]._value = 'up' s['gpio4.state']._value = 'none' for g in (5, 6, 7): s['gpio{}.mode'.format(g)]._value = 'out' s['gpio{}.state'.format(g)]._value = 'high' assert set(s['gpio0.mode'].output()) == { 'gpio=2,3=ip,pu', 'gpio=4=ip,np', 'gpio=5-7=op,dh', } assert list(s['gpio0.state'].output()) == [] with pytest.raises(DelegatedOutput) as exc: list(s['gpio2.mode'].output()) assert exc.value.master == 'gpio0.mode' with pytest.raises(DelegatedOutput) as exc: list(s['gpio2.state'].output()) assert exc.value.master == 'gpio0.mode' def test_parse_gpio(): with pytest.raises(ValueError): parse_gpio('0') with pytest.raises(ValueError): parse_gpio('4=foo') assert parse_gpio('0=op,dh') == ({0}, 'out', 'high') assert parse_gpio('0=op,np') == ({0}, 'out', 'low') assert parse_gpio('0=ip,dh') == ({0}, 'in', 'none') assert parse_gpio('1,2- 3=op,dh') == ({1}, 'out', 'high') assert parse_gpio('1,2-a=op,dh') == ({1}, 'out', 'high') assert parse_gpio('1,2--3=op,dh') == ({1}, 'out', 'high') assert parse_gpio('1,2, 3=op,dh') == ({1, 2}, 'out', 'high') assert parse_gpio('1,2,-3=op,dh') == ({1, 2}, 'out', 'high') assert parse_gpio('1,2,-3--4=op,dh') == ({1, 2}, 'out', 'high') pibootctl-0.5.2/tests/test_store.py000066400000000000000000000363221372751746400174520ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2019, 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . from pathlib import Path from unittest import mock from zipfile import ZipFile from datetime import datetime import pytest from pibootctl.parser import * from pibootctl.setting import * from pibootctl.store import * @pytest.fixture() def boot_path(request, tmpdir): return Path(str(tmpdir)) @pytest.fixture() def store_path(request, boot_path): return boot_path / 'pibootctl' cond_all = BootConditions() cond_none = cond_all.evaluate('none') def test_singleton_reprs(): assert repr(Current) == 'Current' assert repr(Default) == 'Default' def test_store_container(boot_path, store_path): store = Store(boot_path, store_path) (boot_path / 'config.txt').write_text("""\ dtparam=i2c=on dtparam=spi=on """) store_path.mkdir() with (store_path / 'foo.zip').open('wb') as f: with ZipFile(f, 'w') as z: z.comment = ('pibootctl:0:' + store[Current].hash).encode('ascii') store[Current].files['config.txt'].add_to_zip(z) with (store_path / 'invalid.zip').open('wb') as f: with ZipFile(f, 'w') as z: z.comment = ('pibootctl:999:' + store[Current].hash).encode('ascii') store[Current].files['config.txt'].add_to_zip(z) assert len(store) == 3 assert Current in store assert Default in store assert 'foo' in store assert 'bar' not in store assert set(store) == {Default, Current, 'foo'} with pytest.raises(KeyError): store['bar'] def test_store_bad_arc(boot_path, store_path): store = Store(boot_path, store_path) (boot_path / 'config.txt').write_text("""\ dtparam=i2c=on dtparam=spi=on """) store_path.mkdir() with (store_path / 'foo.zip').open('wb') as f: with ZipFile(f, 'w') as z: z.comment = b'pibootctl:badver:' z.writestr('config.txt', b'') with pytest.raises(KeyError): store['foo'] with (store_path / 'foo.zip').open('wb') as f: with ZipFile(f, 'w') as z: z.comment = b'pibootctl:0:badhash' z.writestr('config.txt', b'') with pytest.raises(ValueError): store['foo'] with (store_path / 'foo.zip').open('wb') as f: with ZipFile(f, 'w') as z: z.comment = b'pibootctl:0:' + b'h' * 40 z.writestr('config.txt', b'') with pytest.raises(ValueError): store['foo'] def test_store_getitem(boot_path, store_path): store = Store(boot_path, store_path) content = b"""\ dtparam=i2c=on dtparam=spi=on hdmi_group=1 hdmi_mode=4 """ (boot_path / 'config.txt').write_bytes(content) current = store[Current] assert current.path == boot_path assert current.config_root == 'config.txt' d = datetime.fromtimestamp( (boot_path / 'config.txt').stat().st_mtime) d = d.replace( year=max(1980, d.year), second=d.second // 2 * 2, microsecond=0) assert current.timestamp == d assert current.hash == '5179ada9ed2534c0d228d950c65d4d58babef1cd' assert current.settings['i2c.enabled'].value assert current.settings['spi.enabled'].value assert not current.settings['audio.enabled'].value assert current.settings['video.hdmi0.group'].value == 1 assert current.settings['video.hdmi0.mode'].value == 4 assert current.settings['video.hdmi1.group'].value == 0 assert current.settings['video.hdmi1.mode'].value == 0 assert current.files['config.txt'].content == content def test_store_getitem_with_includes(boot_path, store_path): store = Store(boot_path, store_path) config_txt = b"""\ [all] dtparam=i2c=on dtparam=spi=on [none] include inc.txt """ inc_txt = b"""\ [all] hdmi_group=1 hdmi_mode=4 """ (boot_path / 'config.txt').write_bytes(config_txt) (boot_path / 'inc.txt').write_bytes(inc_txt) current = store[Current] assert current.path == boot_path assert current.config_root == 'config.txt' d = datetime.fromtimestamp( (boot_path / 'inc.txt').stat().st_mtime) d = d.replace( year=max(1980, d.year), second=d.second // 2 * 2, microsecond=0) assert current.timestamp == d assert current.hash == 'e76f44e09c3e3e36022c248cb71924e2af76a18b' assert current.settings['i2c.enabled'].value assert current.settings['spi.enabled'].value assert not current.settings['audio.enabled'].value assert current.settings['video.hdmi0.group'].value == 0 assert current.settings['video.hdmi0.mode'].value == 0 assert current.settings['video.hdmi1.group'].value == 0 assert current.settings['video.hdmi1.mode'].value == 0 assert current.files['config.txt'].content == config_txt assert current.files['inc.txt'].content == inc_txt def test_store_setitem(boot_path, store_path): store = Store(boot_path, store_path) content = [ 'dtparam=i2c=on\n', 'dtparam=spi=on\n', ] (boot_path / 'config.txt').write_text(''.join(content)) (boot_path / 'edid.dat').write_bytes(b'\x00\x00\x00\xFF') assert len(store) == 2 assert 'foo' not in store store['foo'] = store[Current] assert store_path.is_dir() assert (store_path / 'foo.zip').is_file() assert len(store) == 3 assert 'foo' in store assert store['foo'].hash == store[Current].hash assert store['foo'].files == store[Current].files (boot_path / 'config.txt').write_text('') assert store['foo'].hash != store[Current].hash assert store['foo'].files != store[Current].files store[Current] = store['foo'] assert store['foo'].hash == store[Current].hash assert store['foo'].files == store[Current].files with pytest.raises(KeyError): store[Default] = store[Current] with pytest.raises(KeyError): store[''] = store[Current] def test_store_delitem(boot_path, store_path): store = Store(boot_path, store_path) (boot_path / 'config.txt').write_text("""\ dtparam=i2c=on dtparam=spi=on """) store['foo'] = store[Current] assert len(store) == 3 assert 'foo' in store del store['foo'] assert len(store) == 2 assert 'foo' not in store assert not (store_path / 'foo.zip').exists() with pytest.raises(KeyError): del store['bar'] with pytest.raises(KeyError): del store[Current] with pytest.raises(KeyError): del store[Default] def test_store_restore_default(boot_path, store_path): store = Store(boot_path, store_path) (boot_path / 'config.txt').write_text("""\ dtparam=i2c=on dtparam=spi=on """) store['foo'] = store[Current] store[Current] = store[Default] assert not (boot_path / 'config.txt').exists() store[Current] = store['foo'] assert (boot_path / 'config.txt').exists() def test_store_active(boot_path, store_path): store = Store(boot_path, store_path) (boot_path / 'config.txt').write_text("""\ dtparam=i2c=on dtparam=spi=on """) assert len(store) == 2 assert store.active is None store_path.mkdir() with (store_path / 'foo.zip').open('wb') as f: with ZipFile(f, 'w') as z: z.comment = ('pibootctl:0:' + store[Current].hash).encode('ascii') store[Current].files['config.txt'].add_to_zip(z) assert len(store) == 3 assert store.active == 'foo' def test_store_mutable_update(boot_path, store_path): store = Store(boot_path, store_path, mutable_files={'config.txt', 'syscfg.txt'}) (boot_path / 'config.txt').write_text("""\ # Header goes here include syscfg.txt """) (boot_path / 'syscfg.txt').write_text("""\ dtparam=i2c=on dtparam=spi=on """) current = store[Current] mutable = current.mutable() mutable.update({'i2c.enabled': None, 'camera.enabled': True}, cond_all) assert mutable.files['config.txt'].content.decode('ascii') == """\ # Header goes here include syscfg.txt start_x=1 """ assert mutable.files['syscfg.txt'].content.decode('ascii') == """\ dtparam=spi=on """ def test_store_mutable_ineffective(boot_path, store_path): store = Store(boot_path, store_path, mutable_files={'config.txt', 'syscfg.txt'}) (boot_path / 'config.txt').write_text("""\ include syscfg.txt include usercfg.txt """) (boot_path / 'syscfg.txt').write_text("""\ dtparam=i2c=on dtparam=spi=on """) (boot_path / 'usercfg.txt').write_text("""\ dtparam=i2c=on """) current = store[Current] mutable = current.mutable() with pytest.raises(IneffectiveConfiguration) as exc_info: mutable.update({'i2c.enabled': None}, cond_all) assert len(exc_info.value.diff) == 1 assert str(exc_info.value) == "Failed to set 1 setting(s)" assert mutable.files['config.txt'].content.decode('ascii') == """\ include syscfg.txt include usercfg.txt """ assert mutable.files['syscfg.txt'].content.decode('ascii') == """\ dtparam=spi=on """ def test_store_new_section(boot_path, store_path): store = Store(boot_path, store_path) (boot_path / 'config.txt').write_text("""\ [pi4] max_framebuffers=2 """) current = store[Current] mutable = current.mutable() mutable.update({'camera.enabled': True}, cond_all) assert mutable.files['config.txt'].content.decode('ascii') == """\ [pi4] max_framebuffers=2 [all] start_x=1 """ def test_store_new_sections(boot_path, store_path): with mock.patch('pibootctl.parser.get_board_serial') as get_board_serial: get_board_serial.return_value = 0xf000000d store = Store(boot_path, store_path) (boot_path / 'config.txt').write_text("""\ [pi4] max_framebuffers=2 """) current = store[Current] mutable = current.mutable() cond_serial = cond_all._replace(serial=0xf000000d) mutable.update({'gpu.mem': 128}, cond_serial) assert mutable.files['config.txt'].content.decode('ascii') == """\ [pi4] max_framebuffers=2 [all] [0xF000000D] gpu_mem=128 """ def test_store_comment_lines(boot_path, store_path): store = Store(boot_path, store_path, mutable_files={'config.txt', 'syscfg.txt'}, comment_lines=True) (boot_path / 'config.txt').write_text("""\ # Header goes here include syscfg.txt """) (boot_path / 'syscfg.txt').write_text("""\ # Enable I2C dtparam=i2c=on # Enable SPI dtparam=spi=on """) current = store[Current] mutable = current.mutable() mutable.update({'i2c.enabled': None, 'camera.enabled': True}, cond_all) assert mutable.files['config.txt'].content.decode('ascii') == """\ # Header goes here include syscfg.txt start_x=1 """ assert mutable.files['syscfg.txt'].content.decode('ascii') == """\ # Enable I2C #dtparam=i2c=on # Enable SPI dtparam=spi=on """ def test_store_no_recomment_lines(boot_path, store_path): store = Store(boot_path, store_path, comment_lines=True) (boot_path / 'config.txt').write_text("""\ # Header goes here gpio=5-7=op,dl """) current = store[Current] mutable = current.mutable() mutable.update({ 'gpio5.state': 'high', 'gpio6.state': 'high', 'gpio7.state': 'high', }, cond_all) assert mutable.files['config.txt'].content.decode('ascii') == """\ # Header goes here #gpio=5-7=op,dl gpio=5-7=op,dh """ def test_store_uncomment_lines(boot_path, store_path): store = Store(boot_path, store_path, comment_lines=True) (boot_path / 'config.txt').write_text("""\ # Header goes here # Enable the camera #start_x=1 #gpu_mem=128 """) current = store[Current] mutable = current.mutable() mutable.update({'gpu.mem': 128, 'camera.enabled': True}, cond_all) assert mutable.files['config.txt'].content.decode('ascii') == """\ # Header goes here # Enable the camera start_x=1 gpu_mem=128 """ def test_store_uncomment_bad_section(boot_path, store_path): store = Store(boot_path, store_path, comment_lines=True) (boot_path / 'config.txt').write_text("""\ [pi4] #start_x=1 #gpu_mem=128 """) current = store[Current] mutable = current.mutable() mutable.update({'gpu.mem': 128, 'camera.enabled': True}, cond_all) assert mutable.files['config.txt'].content.decode('ascii') == """\ [pi4] #start_x=1 #gpu_mem=128 [all] start_x=1 gpu_mem=128 """ def test_store_dont_touch_lines_out_of_context(boot_path, store_path): with mock.patch('pibootctl.parser.get_board_types') as get_board_type: get_board_type.return_value = {'pi0', 'pi0w'} store = Store(boot_path, store_path, comment_lines=True) (boot_path / 'config.txt').write_text("""\ [all] gpio=18,23=op,dh """) current = store[Current] cond_model = cond_all.evaluate('pi0') mutable = current.mutable() mutable.update({'gpio18.mode': 'in', 'gpio18.state': 'up'}, cond_model) assert mutable.files['config.txt'].content.decode('ascii') == """\ [all] gpio=18,23=op,dh [pi0] gpio=18=ip,pu gpio=23=op,dh """ def test_settings_container(): settings = Settings() assert len([s for s in settings]) == len(settings) assert 'video.hdmi0.mode' in settings assert isinstance(settings['video.hdmi0.mode'], CommandDisplayMode) def test_default_config(boot_path, store_path): store = Store(boot_path, store_path) default = store[Default] assert default.files == {} assert default.hash == 'da39a3ee5e6b4b0d3255bfef95601890afd80709' assert default.timestamp == datetime(1970, 1, 1) def test_settings_copy(): settings = Settings() copy = settings.copy() assert len(settings) == len(copy) assert settings is not copy assert set(s for s in settings) == set(s for s in copy) assert all(settings[name] is not copy[name] for name in settings) def test_settings_diff(boot_path, store_path): store = Store(boot_path, store_path) (boot_path / 'config.txt').write_text("""\ dtparam=i2c=on dtparam=spi=on hdmi_group=1 hdmi_mode=4 """) default = store[Default].settings current = store[Current].settings assert default.diff(current) == { (default[name], current[name]) for name in ( 'i2c.enabled', 'spi.enabled', 'video.hdmi0.group', 'video.hdmi0.mode', ) } def test_settings_filter(boot_path, store_path): store = Store(boot_path, store_path) (boot_path / 'config.txt').write_text("""\ dtparam=i2c=on dtparam=spi=on """) current = store[Current].settings assert 'i2c.enabled' in current assert 'video.hdmi0.group' in current modified = current.modified() assert modified is not current assert 'i2c.enabled' in modified assert 'video.hdmi0.group' not in modified with pytest.raises(KeyError): modified['video.hdmi0.group'] assert len(modified) < len(current) filtered = modified.filter('spi.*') assert 'i2c.enabled' not in filtered assert 'video.hdmi0.group' not in filtered assert len(filtered) < len(modified) pibootctl-0.5.2/tests/test_term.py000066400000000000000000000146331372751746400172660ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2019, 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . import io import argparse from unittest import mock import pytest from pibootctl.term import * def test_term_is_dumb(): with mock.patch('os.isatty') as m: m.return_value = False assert term_is_dumb() m.return_value = True assert not term_is_dumb() with mock.patch('sys.stdout.fileno') as m: m.side_effect = OSError assert term_is_dumb() def test_term_size(): with mock.patch('fcntl.ioctl') as ioctl: ioctl.side_effect = [ OSError, b'B\x00\xf0\x00\x00\x00\x00\x00', ] assert term_size() == (240, 66) with mock.patch('os.ctermid') as ctermid, mock.patch('os.open') as os_open: ioctl.side_effect = [ OSError, OSError, OSError, b'C\x00\xf0\x00\x00\x00\x00\x00', ] assert term_size() == (240, 67) with mock.patch('os.environ', {}) as environ: ioctl.side_effect = OSError os_open.side_effect = OSError environ['COLUMNS'] = 240 environ['LINES'] = 68 assert term_size() == (240, 68) environ.clear() assert term_size() == (80, 24) def test_error_handler_ops(): handler = ErrorHandler() assert len(handler) == 3 assert SystemExit in handler assert KeyboardInterrupt in handler handler[Exception] = (handler.exc_message, 1) assert len(handler) == 4 assert handler[Exception] == (handler.exc_message, 1) del handler[Exception] assert len(handler) == 3 handler.clear() assert len(handler) == 0 def test_error_handler_sysexit(capsys): handler = ErrorHandler() with pytest.raises(SystemExit) as exc: handler(SystemExit, SystemExit(4), None) assert exc.value.args[0] == 4 captured = capsys.readouterr() assert not captured.out assert not captured.err def test_error_handler_ctrl_c(capsys): handler = ErrorHandler() with pytest.raises(SystemExit) as exc: handler(KeyboardInterrupt, KeyboardInterrupt(3), None) assert exc.value.args[0] == 2 captured = capsys.readouterr() assert not captured.out assert not captured.err def test_error_handler_value_error(capsys): handler = ErrorHandler() handler[Exception] = (handler.exc_message, 1) with pytest.raises(SystemExit) as exc: handler(ValueError, ValueError('Wrong value'), None) assert exc.value.args[0] == 1 captured = capsys.readouterr() assert not captured.out assert captured.err == 'Wrong value\n' def test_error_handler_arg_error(capsys): handler = ErrorHandler() with pytest.raises(SystemExit) as exc: handler(argparse.ArgumentError, argparse.ArgumentError(None, 'Invalid option'), None) assert exc.value.args[0] == 2 captured = capsys.readouterr() assert not captured.out assert captured.err == 'Invalid option\nTry the --help option for more information.\n' def test_error_handler_traceback(capsys): handler = ErrorHandler() with mock.patch('traceback.format_exception') as m: m.return_value = ['Traceback lines\n', 'from some file\n', 'with some context\n'] with pytest.raises(SystemExit) as exc: handler(ValueError, ValueError('Another wrong value'), {}) assert exc.value.args[0] == 1 captured = capsys.readouterr() assert not captured.out assert captured.err == 'Traceback lines\nfrom some file\nwith some context\n' def test_term_pager_dumb(capsys): with mock.patch('pibootctl.term.term_is_dumb') as dumb: dumb.return_value = True with pager(): print('dumb terminal passes thru') captured = capsys.readouterr() assert captured.out == 'dumb terminal passes thru\n' assert captured.err == '' def test_term_pager_no_pager(capsys): with mock.patch('pibootctl.term.term_is_dumb') as dumb, \ mock.patch('subprocess.Popen') as popen: dumb.return_value = False popen.side_effect = OSError(2, "File not found") with pager(): print('foo') captured = capsys.readouterr() assert captured.out == 'foo\n' assert captured.err == '' def test_term_pager_broken_pager(capsys): with mock.patch('pibootctl.term.term_is_dumb') as dumb, \ mock.patch('subprocess.Popen') as popen: dumb.return_value = False popen.side_effect = OSError(1, "Permission denied") with pager(): print('foo bar') captured = capsys.readouterr() assert captured.out == 'foo bar\n' assert captured.err == """\ Failed to execute pager: pager [Errno 1] Permission denied Failed to execute pager: less [Errno 1] Permission denied Failed to execute pager: more [Errno 1] Permission denied """ def test_term_pager_working(capsys, tmpdir): with mock.patch('pibootctl.term.term_is_dumb') as dumb, \ mock.patch('subprocess.Popen') as popen: dumb.return_value = False popen.return_value = mock.Mock(stdin=tmpdir.join('pager.out').open('wb')) with pager(): print('foo bar baz') captured = capsys.readouterr() assert captured.out == '' assert captured.err == '' assert tmpdir.join('pager.out').read_binary() == b'foo bar baz\n' def test_term_pager_override(capsys, tmpdir): with mock.patch('pibootctl.term.term_is_dumb') as dumb: dumb.return_value = False with pager(False): print('terminal passes thru when forced') captured = capsys.readouterr() assert captured.out == 'terminal passes thru when forced\n' assert captured.err == '' pibootctl-0.5.2/tests/test_userstr.py000066400000000000000000000061741372751746400200270ustar00rootroot00000000000000# Copyright (c) 2020 Canonical Ltd. # Copyright (c) 2020 Dave Jones # # This file is part of pibootctl. # # pibootctl is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pibootctl is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pibootctl. If not, see . import pytest from pibootctl.userstr import * def test_to_bool(): assert to_bool(None) is None assert to_bool('') is False assert to_bool('1') is True assert to_bool('0') is True assert to_bool(UserStr('')) is None assert to_bool(UserStr('1')) is True assert to_bool(UserStr('YES')) is True assert to_bool(UserStr(' true ')) is True assert to_bool(UserStr(' 0')) is False assert to_bool(UserStr('n')) is False assert to_bool(UserStr('false ')) is False assert to_bool(UserStr(' ')) is None with pytest.raises(ValueError): to_bool(UserStr('foo')) def test_to_int(): assert to_int(None) is None assert to_int(1) == 1 assert to_int('1') == 1 assert to_int(' 10001') == 10001 assert to_int(3.0) == 3 assert to_int(UserStr('')) is None assert to_int(UserStr('1')) == 1 assert to_int(UserStr(' 10001 ')) == 10001 assert to_int(UserStr(' 0XA')) == 0xa assert to_int(UserStr('0xd00dfeed ')) == 0xd00dfeed with pytest.raises(ValueError): to_int(UserStr('d00dfeed')) with pytest.raises(ValueError): to_int(UserStr(' foo')) with pytest.raises(ValueError): to_int(UserStr('0o644')) def test_to_float(): assert to_float(None) is None assert to_float(1) == 1.0 assert to_float(1.5) == 1.5 assert to_float('1.5') == 1.5 assert to_float(' 1e4') == 10000.0 assert to_float(UserStr('')) is None assert to_float(UserStr('1.5')) == 1.5 assert to_float(UserStr(' 1e4 ')) == 10000.0 with pytest.raises(ValueError): to_float(UserStr('0x10')) with pytest.raises(ValueError): to_float(UserStr(' foo')) with pytest.raises(ValueError): to_float(UserStr('0o644')) def test_to_str(): assert to_str(None) is None assert to_str('') == '' assert to_str('foo') == 'foo' assert to_str(' foo') == ' foo' assert to_str(1) == '1' assert to_str(UserStr('')) is None assert to_str(UserStr(' ')) == '' assert to_str(UserStr(' foo')) == 'foo' def test_to_list(): assert to_list(None) is None assert to_list('') == [''] assert to_list([]) == [] assert to_list('foo') == ['foo'] assert to_list('foo,bar') == ['foo', 'bar'] assert to_list(UserStr('')) is None assert to_list(UserStr(' ')) == [''] assert to_list(UserStr(' foo')) == ['foo'] assert to_list(UserStr('foo,bar')) == ['foo', 'bar'] pibootctl-0.5.2/tox.ini000066400000000000000000000003001372751746400150410ustar00rootroot00000000000000[tox] envlist = py{35,36,37,38} [testenv] setenv = COVERAGE_FILE=.coverage.{envname} passenv = COVERAGE_* deps = .[test] usedevelop = True commands = make test whitelist_externals = make