Repository: bin456789/reinstall Branch: main Commit: e104735538f0 Files: 41 Total size: 585.3 KB Directory structure: gitextract_8gs5ci9z/ ├── .editorconfig ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ ├── run_reinstall.yml │ └── sync_to_cnb.yml ├── LICENSE ├── README.en.md ├── README.md ├── cloud-init-fix-onlink.sh ├── cloud-init.yaml ├── debian.cfg ├── fix-eth-name.initd ├── fix-eth-name.service ├── fix-eth-name.sh ├── frpc-example.toml ├── frpc.service ├── get-frpc-url.sh ├── get-xda.sh ├── initrd-network.sh ├── logviewer-nginx.conf ├── logviewer.html ├── redhat.cfg ├── reinstall.bat ├── reinstall.sh ├── resize.sh ├── trans.sh ├── ttys.sh ├── ubuntu-storage-early.sh ├── ubuntu.yaml ├── windows-allow-ping.bat ├── windows-change-rdp-port.bat ├── windows-del-gpo.bat ├── windows-driver-utils.sh ├── windows-frpc.bat ├── windows-frpc.xml ├── windows-resize.bat ├── windows-set-netconf.bat ├── windows-setup.bat ├── windows.xml └── wmic.ps1 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = space indent_size = 4 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [{windows.xml,windows-*.xml}] end_of_line = crlf [windows-frpc.xml] charset = utf-16-le indent_size = 2 [*.{bat,cmd,ps1}] end_of_line = crlf [*.ps1] charset = utf-8-bom [*.{yml,yaml}] indent_size = 2 [reinstall.sh] shell_variant = bash ================================================ FILE: .github/FUNDING.yml ================================================ github: bin456789 ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report (问题反馈) about: Create a report to help us improve title: '' labels: '' assignees: '' --- 原来的系统 (Original system): 要安装的系统 (System to be installed): 遇到的问题 (Issue): ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request (功能请求) about: Suggest an idea for this project title: '' labels: '' assignees: '' --- ================================================ FILE: .github/workflows/run_reinstall.yml ================================================ name: 运行主程序 on: workflow_dispatch: push: paths: - "reinstall.*" jobs: run: name: 运行主程序 strategy: matrix: os: [ubuntu-latest, windows-latest] include: - os: ubuntu-latest command: sudo bash reinstall.sh --debug --password 123@@@ - os: windows-latest command: ./reinstall.bat --debug --password 123@@@ runs-on: ${{ matrix.os }} steps: - run: | git config --global core.autocrlf false - uses: actions/checkout@v4 - run: | # ${{ matrix.command }} centos ${{ matrix.command }} almalinux # ${{ matrix.command }} rocky # ${{ matrix.command }} fedora # ${{ matrix.command }} oracle ${{ matrix.command }} ubuntu ${{ matrix.command }} debian ${{ matrix.command }} debian --ci # ${{ matrix.command }} kali # ${{ matrix.command }} alpine # ${{ matrix.command }} opensuse # ${{ matrix.command }} arch # ${{ matrix.command }} gentoo ${{ matrix.command }} netboot.xyz ${{ matrix.command }} dd --img=https://download.opensuse.org/tumbleweed/appliances/openSUSE-MicroOS.x86_64-SelfInstall.raw.xz ${{ matrix.command }} windows --image-name='Windows Server blah' --iso https://aka.ms/HCIReleaseImage ${{ matrix.command }} reset # 测试失败例子 # ${{ matrix.command }} wrong-os # ${{ matrix.command }} dd --img=https://github.com/ # ${{ matrix.command }} windows --iso=https://github.com/ --image-name=abc ================================================ FILE: .github/workflows/sync_to_cnb.yml ================================================ name: 同步到 CNB on: workflow_dispatch: push: jobs: run: name: 同步到 CNB runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: yesolutions/mirror-action@master with: REMOTE: https://cnb.cool/bin456789/reinstall.git GIT_USERNAME: cnb GIT_PASSWORD: ${{ secrets.CNB_TOKEN }} PUSH_ALL_REFS: false ================================================ FILE: LICENSE ================================================ 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 . ================================================ FILE: README.en.md ================================================ # reinstall [![Codacy](https://img.shields.io/codacy/grade/dc679a17751448628fe6d8ac35e26eed?logo=Codacy&label=Codacy&style=flat-square)](https://app.codacy.com/gh/bin456789/reinstall/dashboard) [![CodeFactor](https://img.shields.io/codefactor/grade/github/bin456789/reinstall?logo=CodeFactor&logoColor=white&label=CodeFactor&style=flat-square)](https://www.codefactor.io/repository/github/bin456789/reinstall) [![Lines of Code](https://aschey.tech/tokei/github/bin456789/reinstall?category=code&label=Lines%20of%20Code&style=flat-square)](https://github.com/aschey/vercel-tokei) One-Click system reinstallation script for VPS [中文](README.md) ## Introduction - One-click reinstallation to Linux: Supports 19 common distributions. - One-click reinstallation to Windows: Uses the official original ISO instead of custom images. The script can automatically fetch the ISO link and installs public cloud drivers like `VirtIO`. - Supports reinstallation in any direction, i.e., `Linux to Linux`, `Linux to Windows`, `Windows to Windows`, `Windows to Linux` - Automatically configures IP and intelligently sets it as static or dynamic. Supports `/32`, `/128`, `gateway outside subnet`, `IPv6 only`, `IPv4/IPv6 on different NIC` - Specially optimized for low-spec servers, requires less memory than the official netboot - Uses partition table ID to identify hard drives throughout the process, ensuring no wrong disk is written - Supports BIOS and EFI boot, and ARM Server - No homemades image included, all resources are obtained in real-time from mirror sites If this helped you, you can buy me a milk tea. [![Donate](https://img.shields.io/badge/Donate-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#EA4AAA)](https://github.com/sponsors/bin456789) [![Sponsors](https://raw.githubusercontent.com/bin456789/sponsors/refs/heads/master/sponsors.svg)](https://github.com/sponsors/bin456789) ### Feedback [![GitHub Issues](https://img.shields.io/badge/GitHub-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/bin456789/reinstall/issues) [![Telegram Group](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/reinstall_os) ## Quick Start - [Download](#download-current-system-is--linux) - [Feature 1. One-click reinstallation to Linux](#feature-1-install--linux) - [Feature 2. One-click DD Raw image to hard disk](#feature-2-dd-raw-image-to-hard-disk) - [Feature 3. One-click reboot to Alpine Live OS](#feature-3-reboot-to--alpine-live-os) - [Feature 4. One-click reboot to netboot.xyz](#feature-4-reboot-to--netbootxyz) - [Feature 5. One-click reinstallation to Windows](#feature-5-install--windows-iso) - [Cancel the reinstallation](#cancel-the-reinstallation) ## System Requirements The original system can be any system listed in the table. The system requirements for the target system are as follows: | System | Version | Memory | Disk | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | --------- | ---------------- | | Alpine | 3.20, 3.21, 3.22, 3.23 | 256 MB | 1 GB | | Debian | 9, 10, 11, 12, 13 | 256 MB | 1 ~ 1.5 GB ^ | | Kali | Rolling | 256 MB | 1 ~ 1.5 GB ^ | | Ubuntu | 16.04 LTS - 24.04 LTS, 25.10 | 512 MB \* | 2 GB | | Anolis | 7, 8, 23 | 512 MB \* | 5 GB | | RHEL   AlmaLinux   Rocky   Oracle | 8, 9, 10 | 512 MB \* | 5 GB | | OpenCloudOS | 8, 9, Stream 23 | 512 MB \* | 5 GB | | CentOS Stream | 9, 10 | 512 MB \* | 5 GB | | Fedora | 42, 43 | 512 MB \* | 5 GB | | openEuler | 20.03 LTS - 24.03 LTS, 25.09 | 512 MB \* | 5 GB | | openSUSE | Leap 15.6, 16.0, Tumbleweed (Rolling) | 512 MB \* | 5 GB | | NixOS | 25.11 | 512 MB | 5 GB | | Arch | Rolling | 512 MB | 5 GB | | Gentoo | Rolling | 512 MB | 5 GB | | AOSC OS | Rolling | 512 MB | 5 GB | | fnOS | 1 | 512 MB | 8 GB | | Windows (DD) | Any | 512 MB | Depends on image | | Windows (ISO) | Vista, 7, 8.x (Server 2008 - 2012 R2) | 512 MB | 25 GB | | Windows (ISO) | 10, 11 (Server 2016 - 2025) | 1 GB | 25 GB | \* Indicates installation using cloud images, not traditional network installation. ^ Indicates requiring either 256 MB memory + 1.5 GB disk, or 512 MB memory + 1 GB disk > [!WARNING] > > In theory it also supports dedicated servers and PCs > > but if you can use IPMI or a USB drive, this script is not recommended. > [!WARNING] > > ❌ This script does not support OpenVZ or LXC virtual machines. > > Please use instead. ## Download (Current system is Linux) For server outside China: ```bash curl -O https://raw.githubusercontent.com/bin456789/reinstall/main/reinstall.sh || wget -O ${_##*/} $_ ``` For server inside China: ```bash curl -O https://cnb.cool/bin456789/reinstall/-/git/raw/main/reinstall.sh || wget -O ${_##*/} $_ ``` ## Download (Current system is Windows) > [!IMPORTANT] > Before proceeding, please disable the 'Real-time protection' feature in `Windows Defender`. This feature may prevent `certutil` from downloading any files.
Resolving Script Download Issues on Windows 7 Due to lack of support for TLS 1.2, SHA-256, or outdated root certificates, Windows Vista, 7, and Server 2008 (R2) may not be able to download scripts automatically. Manual downloading is required, as follows: Use Internet Explorer (enable TLS 1.2 in IE's advanced settings first) to download, or use Remote Desktop to save the following two files into the same directory: - - To use, run the downloaded `reinstall.bat`.
For server outside China: ```batch certutil -urlcache -f -split https://raw.githubusercontent.com/bin456789/reinstall/main/reinstall.bat ``` For server inside China: ```batch certutil -urlcache -f -split https://cnb.cool/bin456789/reinstall/-/git/raw/main/reinstall.bat ``` ## Usage **All features** can be used on both Linux and Windows. - on Linux, run `bash reinstall.sh ...` - on Windows, first run `cmd`, then run `.\reinstall.bat ...` - If the link in the parameter contains special characters, it should be enclosed in `""`, not `''`. ### Feature 1: Install Linux > [!CAUTION] > > This feature will erase **the entire hard disk** of the current system (including other partitions)! > > If the script was run by mistake, you can run `bash reinstall.sh reset` before rebooting to cancel the reinstallation operation. - Username `root`. The script prompts for a password. If left blank, a random one is generated. - When installing the latest version, the version number does not need to be specified. - Maximizes disk space usage: no boot partition (except for Fedora) and no swap partition. - Automatically selects different optimized kernels based on machine type, such as `Cloud` or `HWE` kernels. - When installing Red Hat, you must provide the `qcow2` image link obtained from . You can also install `qcow2` of other RHEL-based OS, such as `Alibaba Cloud Linux` and `TencentOS Server`. - After reinstallation, if you need to change the SSH port or switch to key-based login, make sure to also modify the files inside `/etc/ssh/sshd_config.d/`. ```bash bash reinstall.sh anolis 7|8|23 rocky 8|9|10 oracle 8|9|10 almalinux 8|9|10 opencloudos 8|9|23 centos 9|10 fnos 1 nixos 25.11 fedora 42|43 debian 9|10|11|12|13 alpine 3.20|3.21|3.22|3.23 opensuse 15.6|16.0|tumbleweed openeuler 20.03|22.03|24.03|25.09 ubuntu 16.04|18.04|20.04|22.04|24.04|25.10 [--minimal] kali arch gentoo aosc redhat --img="http://access.cdn.redhat.com/xxx.qcow2" ``` #### Optional Parameters - `--password PASSWORD` Set the password - `--ssh-key KEY` Set up SSH login public key, [formatted as follows](#--ssh-key). When using public key, password is empty. - `--ssh-port PORT` Change the SSH port (for log observation during installation and for the new system) - `--web-port PORT` Change the Web port (for log observation during installation only) - `--frpc-config PATH` Add frpc for intranet tunneling. Parameter can be local filepath or HTTP URL of the configuration file. - `--hold 1` Reboot only into install environment, without running installer, only for SSH connect to test network connection. - `--hold 2` Prevent reboot after installation completes, allowing SSH login to modify system content; the system is mounted at `/target` for Debian/Kali and `/os` for other distros. > [!TIP] > > Can monitor the progress through various methods (SSH, HTTP 80 port, VNC from server provider, serial console). > > Even if errors occur during the installation process, SSH is available for manual recovery. > > If the target system is not Debian/Kali, run `/trans.sh alpine` can automatically recover to Alpine Linux.
Experimental Features Install Debian using a cloud image - Suitable for machines with slower CPUs ```bash bash reinstall.sh debian --ci ``` Install CentOS, AlmaLinux, Rocky, Fedora using ISO - Only supports machines with more than 2G of memory and dynamic IP. - Password is `123@@@`, and the SSH port is `22`; modifying them using parameters is not supported. ```bash bash reinstall.sh centos --installer ``` Install Ubuntu using ISO - Only supports machines with more than 1G of memory and dynamic IP. - Password is `123@@@`, and the SSH port is `22`; modifying them using parameters is not supported. ```bash bash reinstall.sh ubuntu --installer ```
### Feature 2: DD RAW image to hard disk > [!CAUTION] > > This feature will erase **the entire hard disk** of the current system (including other partitions)! > > If the script was run by mistake, you can run `bash reinstall.sh reset` before rebooting to cancel the reinstallation operation. - Supports `raw` and fixed-size `vhd` image formats. Either uncompressed or compressed as `.gz`, `.xz`, `.zst`, `.tar`, `.tar.gz`, `.tar.xz`, `.tar.zst`. - When deploy a Windows image, the system disk will be automatically expanded, and machines with a static IP will have their IP configured, and may take a few minutes after the first boot for the configuration to take effect. - When deploy a Linux image, will **NOT** modify any contents of the image. ```bash bash reinstall.sh dd --img "https://example.com/xxx.xz" ``` #### Optional Parameters - `--allow-ping` Configure Windows Firewall to Allow Ping Responses (DD Windows only) - `--rdp-port PORT` Change RDP port (DD Windows only) - `--ssh-port PORT` Change SSH port (for log observation during installation) - `--web-port PORT` Change Web port (for log observation during installation) - `--frpc-config PATH` Add frpc for intranet tunneling (DD Windows only). Parameter can be local filepath or HTTP URL of the configuration file. - `--cloud-data PATH_OR_URL` Inject cloud-init NoCloud configuration into the DD'd Linux image (DD Linux only) - `--hold 1` Reboot only into install environment, without running installer, only for SSH connect to test network connection. - `--hold 2` Prevent reboot after the DD process finishes. For SSH login to modify system content. The Windows system will be mounted at `/os`, but Linux systems will **NOT** be automatically mounted. > [!TIP] > > `--cloud-data` accepts a local directory path or an HTTP base URL. The directory must contain a `user-data` file; `meta-data` and `network-config` are optional: > > ``` > seed/ > ├── user-data # required > ├── meta-data # optional > └── network-config # optional > ``` > > ```bash > # Local directory > bash reinstall.sh dd --img "https://example.com/xxx.xz" --cloud-data /path/to/seed/ > # HTTP directory > bash reinstall.sh dd --img "https://example.com/xxx.xz" --cloud-data "https://example.com/seed/" > ``` > [!TIP] > > Can monitor the progress through various methods (SSH, HTTP 80 port, VNC from server provider, serial console). > > Even if errors occur during the installation process, SSH is available for manual recovery. > > Or Run `/trans.sh alpine` to automatically recover to Alpine Linux. ### Feature 3: Reboot to Alpine Live OS - You can use SSH to backup/restore disk, manually perform DD operations, partition modifications, manual Alpine installation, and other operations. - Username `root`. The script prompts for a password. If left blank, a random one is generated. > [!TIP] > > Although the script being run is `reinstall`, this feature **does not** delete any data or perform an automatic reinstallation; manual user operation is required. > > If the user does not damage the original system during manual operation, rebooting will return to the original system. ```bash bash reinstall.sh alpine --hold 1 ``` #### Optional Parameters - `--password PASSWORD` Set password - `--ssh-port PORT` Change SSH port - `--ssh-key KEY` Set up SSH login public key, [formatted as follows](#--ssh-key). When using public key, password is empty. - `--frpc-config PATH` Add frpc for intranet tunneling. Parameter can be local filepath or HTTP URL of the configuration file. ### Feature 4: Reboot to netboot.xyz - Can manually install [more systems](https://github.com/netbootxyz/netboot.xyz?tab=readme-ov-file#what-operating-systems-are-currently-available-on-netbootxyz) using vendor backend VNC. > [!TIP] > > Although the script being run is `reinstall`, this feature **does not** delete any data or perform an automatic reinstallation; manual user operation is required. > > If the user does not damage the original system during manual operation, rebooting will return to the original system. ```bash bash reinstall.sh netboot.xyz ``` ![netboot.xyz](https://netboot.xyz/images/netboot.xyz.gif) ### Feature 5: Install Windows ISO ![Windows Installation](https://github.com/bin456789/reinstall/assets/7548515/07c1aea2-1ce3-4967-904f-aaf9d6eec3f7) > [!CAUTION] > > This feature will erase **the entire hard disk** of the current system (including other partitions)! > > If the script was run by mistake, you can run `bash reinstall.sh reset` before rebooting to cancel the reinstallation operation. - Username `administrator`. The script prompts for a password. If left blank, a random one is generated. - If remote login fails, try using the username `.\administrator`. - The machine with a static IP will automatically configure the IP. It may take a few minutes to take effect on the first boot. - Supports ISO images in any language. - Supports bypassing Windows 11 hardware requirements. #### Supported Systems - Windows (Vista ~ 11) - Windows Server (2008 ~ 2025) - Windows Server Essentials - Windows Server (Semi) Annual Channel - Hyper-V Server - Azure Local (Azure Stack HCI) #### Method 1: Let the Script Automatically Search for ISO - The script will search for ISOs from , a site that collects official ISOs. - Only supports ISOs searching for Windows 10, 11, Server 2019, 2022, 2025. ```bash bash reinstall.sh windows \ --image-name "Windows 11 Enterprise LTSC 2024" \ --lang zh-cn ```
Supported languages ```text ar-sa bg-bg cs-cz da-dk de-de el-gr en-gb en-us es-es es-mx et-ee fi-fi fr-ca fr-fr he-il hr-hr hu-hu it-it ja-jp ko-kr lt-lt lv-lv nb-no nl-nl pl-pl pt-pt pt-br ro-ro ru-ru sk-sk sl-si sr-latn-rs sv-se th-th tr-tr uk-ua zh-cn zh-hk zh-tw ```
#### Method 2: Specify the ISO link manually - If you don't know the `--image-name`, you can enter any value. After rebooting, connect via SSH and re-enter the correct value based on the error messages. ```bash bash reinstall.sh windows \ --image-name "Windows 11 Enterprise LTSC 2024 Evaluation" \ --iso "https://go.microsoft.com/fwlink/?linkid=2289029" ``` or Magnet Link ```bash bash reinstall.sh windows \ --image-name "Windows 11 Enterprise LTSC 2024" \ --iso "magnet:?xt=urn:btih:7352bd2db48c3381dffa783763dc75aa4a6f1cff" ```
The following website provides ISO links. - General - - - - - (Need to open it with a non-Windows User-Agent) - - - Evaluation - - - - - - - - Insider Preview - -
#### Optional Parameters - `--password PASSWORD` Set Password - `--allow-ping` Configure Windows Firewall to Allow Ping Responses - `--rdp-port PORT` Change RDP port - `--ssh-port PORT` Change SSH port (for log observation during installation only) - `--web-port PORT` Change Web port (for log observation during installation only) - `--add-driver INF_OR_DIR` Add additional driver, specifying .inf path, or the folder contains .inf file. - The driver must be downloaded to current system first. - This parameter can be set multiple times to add different driver. - `--frpc-config PATH` Add frpc for intranet tunneling. Parameter can be local filepath or HTTP URL of the configuration file. - `--hold 1` Reboot only into install environment, without running installer, only for SSH connect to test network connection. - `--hold 2` Allow SSH connections for modifying `boot.wim`, `install.wim` or other contents before rebooting into the official Windows installation program, with the disk mounted at `/os`. #### The following drivers will automatic download and install as needed, without the need for manual addition - VirtIO ([Community][virtio-virtio], [Alibaba Cloud][virtio-aliyun], [Tencent Cloud][virtio-qcloud], [GCP][virtio-gcp]) - XEN ([~~Community~~][xen-xen] (unsigned), [Citrix][xen-citrix], [AWS][xen-aws]) - AWS ([ENA Network Adapter][aws-ena], [NVME Storage Controller][aws-nvme]) - GCP ([gVNIC Network Adapter][gcp-gvnic], [GGA Display Adapter][gcp-gga]) - Azure ([MANA Network Adapter][azure-mana]) - Intel (VMD Storage Controller: [11th Gen Core][intel-vmd-gen11], [12th-15th Gen Core][intel-vmd-gen12-to-gen15], Network Adapter: [7][intel-nic-7], [8.x][intel-nic-8.1], [10][intel-nic-10], [11][intel-nic-11], [2008 R2][intel-nic-7], [2012][intel-nic-2012], [2012 R2][intel-nic-2012-r2], [2016][intel-nic-2016], [2019][intel-nic-2019], [2022][intel-nic-2022], [2025][intel-nic-2025]) [virtio-virtio]: https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/ [virtio-aliyun]: https://www.alibabacloud.com/help/ecs/user-guide/install-the-virtio-driver-1 [virtio-qcloud]: https://cloud.tencent.com/document/product/213/17815#b84b2032-752c-43c4-a509-73530b8f82ff [virtio-gcp]: https://console.cloud.google.com/storage/browser/gce-windows-drivers-public [xen-xen]: https://xenproject.org/resources/downloads/ [xen-aws]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/xen-drivers-overview.html [xen-citrix]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Upgrading_PV_drivers.html#win2008-citrix-upgrade [aws-ena]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ena-driver-releases-windows.html [aws-nvme]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/nvme-driver-version-history.html [gcp-gvnic]: https://cloud.google.com/compute/docs/networking/using-gvnic [gcp-gga]: https://cloud.google.com/compute/docs/instances/enable-instance-virtual-display [azure-mana]: https://learn.microsoft.com/azure/virtual-network/accelerated-networking-mana-windows [intel-vmd-gen11]: https://www.intel.com/content/www/us/en/download/849933/intel-rapid-storage-technology-driver-installation-software-with-intel-optane-memory-12th-to-13th-gen-platforms.html [intel-vmd-gen12-to-gen15]: https://www.intel.com/content/www/us/en/download/849936/intel-rapid-storage-technology-driver-installation-software-with-intel-optane-memory-12th-to-15th-gen-platforms.html [intel-nic-7]: https://www.intel.com/content/www/us/en/download/15590/intel-network-adapter-driver-for-windows-7-final-release.html [intel-nic-8.1]: https://www.intel.com/content/www/us/en/download/17479/intel-network-adapter-driver-for-windows-8-1.html [intel-nic-10]: https://www.intel.com/content/www/us/en/download/18293/intel-network-adapter-driver-for-windows-10.html [intel-nic-11]: https://www.intel.com/content/www/us/en/download/727998/intel-network-adapter-driver-for-microsoft-windows-11.html [intel-nic-2012]: https://www.intel.com/content/www/us/en/download/16789/intel-network-adapter-driver-for-windows-server-2012.html [intel-nic-2012-r2]: https://www.intel.com/content/www/us/en/download/17480/intel-network-adapter-driver-for-windows-server-2012-r2.html [intel-nic-2016]: https://www.intel.com/content/www/us/en/download/18737/intel-network-adapter-driver-for-windows-server-2016.html [intel-nic-2019]: https://www.intel.com/content/www/us/en/download/19372/intel-network-adapter-driver-for-windows-server-2019.html [intel-nic-2022]: https://www.intel.com/content/www/us/en/download/706171/intel-network-adapter-driver-for-windows-server-2022.html [intel-nic-2025]: https://www.intel.com/content/www/us/en/download/838943/intel-network-adapter-driver-for-windows-server-2025.html #### How to Specify the Image Name `--image-name` An ISO usually contains multiple system editions, such as Home and Pro. Therefore, you need to use `--image-name` to specify the system edition (image name) to install, case-insensitive. You can use tools like DISM, DISM++, or Wimlib to query the image names included in the ISO. Commonly used image names include: ```text Windows 7 Ultimate Windows 11 Pro Windows 11 Enterprise LTSC 2024 Windows Server 2025 SERVERDATACENTER ``` #### How to Use [DISM++](https://github.com/Chuyu-Team/Dism-Multi-language/releases) to Query the Image Names Included in the ISO Open File menu > Open Image File, select the iso to be installed to get the image name (full system name), and all available image names are installable. ![image-name](https://github.com/bin456789/reinstall/assets/7548515/5aae0a9b-61e2-4f66-bb98-d470a6beaac2) > [!WARNING] > Vista (Server 2008) and 32-bit systems may lack drivers. > [!WARNING] > > For Windows 7 (Server 2008 R2) installation: > > 1. EFI-boot machines must enable CSM. > > 2. On Hyper-V (Azure), select Generation 1 VM. > [!WARNING] > > In the Chinese version of Windows 10 LTSC 2021 ISO `zh-cn_windows_10_enterprise_ltsc_2021_x64_dvd_033b7312.iso`, the `wsappx` process may indefinitely consume CPU resources. > > The solution is to update the system patches or manually install the `VCLibs` library . > [!WARNING] > > When installing Windows ISOs released in `May 2022` or later on GCP, the system may repeatedly reboot during the Windows installation (PE) stage. You can resolve this issue using one of the following two methods: > > 1. Add the `--force-boot-mode bios` parameter. The script will install Windows in `BIOS boot + MBR partition table` mode. > > (Optional) After installation, you can convert it to `EFI boot + GPT partition table` using the command `MBR2GPT /convert /allowFullOS`. > > 2. Create a custom RAW image and install it via DD. #### Considerations for Installing Windows on ARM Most ARM machines support installing latest Windows 11. During the installation process, you might encounter a black screen, and the serial console may display `ConvertPages: failed to find range`, but neither issue affects the installation. | Compatibility | Cloud Provider | Instance Type | Issues | | ------------- | -------------- | ----------------------- | ----------------------------------------------------------------------------------- | | ✔️ | Azure | B2pts_v2 | | | ✔️ | AWS | T4g | | | ✔️ | Scaleway | COPARM1 | | | ✔️ | Gcore | | | | ❔ | Alibaba Cloud | g6r, c6r, g8y, c8y, r8y | Might hanging at the boot logo during restart; forced reboot will resolve it. | | ❔ | Oracle Cloud | A1.Flex | Installation success is not guaranteed; newer instances are more likely to succeed. | | ❌ | Google Cloud | t2a | Missing network card drivers | ### Cancel the reinstallation - If the script was run by mistake, you can run this command to cancel the reinstallation operation. - Must be run before rebooting. ```bash bash reinstall.sh reset ``` ## Parameter Format ### --ssh-key - `--ssh-key "ssh-rsa ..."` - `--ssh-key "ssh-ed25519 ..."` - `--ssh-key "ecdsa-sha2-nistp256/384/521 ..."` - `--ssh-key http://path/to/public_key` - `--ssh-key github:your_username` - `--ssh-key gitlab:your_username` - `--ssh-key /path/to/public_key` - `--ssh-key C:\path\to\public_key` ## How to Use an Old Version According to the Law of Bug Conservation, fixing old bugs often introduces new ones. If a new bug occurs, try using an older version to see if it works. Go to and find the old version’s `commit_id` on the right side. ```bash commit_id=xxxxxxx curl -O https://raw.githubusercontent.com/bin456789/reinstall/$commit_id/reinstall.sh || wget -O ${_##*/} $_ sed -i "/^confhome.*main$/s/main/$commit_id/" reinstall.sh bash reinstall.sh ... ``` ## How to Modify the Script for Your Own 1. Fork this repository. 2. Modify the `confhome` and `confhome_cn` at the beginning of `reinstall.sh` and `reinstall.bat`. 3. Make changes to the other code. ## Thanks Thanks to the following businesses for providing free servers. [![Oracle Cloud](https://github.com/bin456789/reinstall/assets/7548515/8b430ed4-8344-4f96-b4da-c2bda031cc90)](https://www.oracle.com/cloud/) [![DartNode](https://github.com/bin456789/reinstall/assets/7548515/435d6740-bcdd-4f3a-a196-2f60ae397f17)](https://dartnode.com/) ================================================ FILE: README.md ================================================ # reinstall [![Codacy](https://img.shields.io/codacy/grade/dc679a17751448628fe6d8ac35e26eed?logo=Codacy&label=Codacy&style=flat-square)](https://app.codacy.com/gh/bin456789/reinstall/dashboard) [![CodeFactor](https://img.shields.io/codefactor/grade/github/bin456789/reinstall?logo=CodeFactor&logoColor=white&label=CodeFactor&style=flat-square)](https://www.codefactor.io/repository/github/bin456789/reinstall) [![Lines of Code](https://aschey.tech/tokei/github/bin456789/reinstall?category=code&label=Lines%20of%20Code&style=flat-square)](https://github.com/aschey/vercel-tokei) 一键 VPS 系统重装脚本 [English](README.en.md) ## 介绍 - 一键重装到 Linux,支持 19 种常见发行版 - 一键重装到 Windows,使用官方原版 ISO 而非自制镜像,脚本支持自动查找 ISO 链接、自动安装 `VirtIO` 等公有云驱动 - 支持任意方向重装,即 `Linux to Linux`、`Linux to Windows`、`Windows to Windows`、`Windows to Linux` - 自动设置 IP,智能设置动静态,支持 `/32`、`/128`、`网关不在子网范围内`、`纯 IPv6`、`IPv4/IPv6 在不同的网卡` - 专门适配低配小鸡,比官方 netboot 需要更少的内存 - 全程用分区表 ID 识别硬盘,确保不会写错硬盘 - 支持 BIOS、EFI 引导,支持 ARM 服务器 - 不含自制包,所有资源均实时从镜像源获得 如果帮到你,可以请我喝奶茶。 [![Donate](https://img.shields.io/badge/Donate-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#EA4AAA)](https://github.com/sponsors/bin456789) [![Sponsors](https://raw.githubusercontent.com/bin456789/sponsors/refs/heads/master/sponsors.svg)](https://github.com/sponsors/bin456789) ### 反馈 [![GitHub Issues](https://img.shields.io/badge/GitHub-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/bin456789/reinstall/issues) [![Telegram Group](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/reinstall_os) ## 快速开始 - [下载](#下载当前系统是--linux) - [功能 1. 一键重装到 Linux](#功能-1-安装--linux) - [功能 2. 一键 DD Raw 镜像到硬盘](#功能-2-dd-raw-镜像到硬盘) - [功能 3. 一键引导到 Alpine Live OS 内存系统](#功能-3-重启到--alpine-live-os内存系统) - [功能 4. 一键引导到 netboot.xyz](#功能-4-重启到--netbootxyz) - [功能 5. 一键重装到 Windows](#功能-5-安装--windows-iso) - [取消重装](#取消重装) ## 系统要求 原系统可以是表格中的任意系统 目标系统的配置要求如下: | 系统 | 版本 | 内存 | 硬盘 | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | --------- | ------------ | | Alpine | 3.20, 3.21, 3.22, 3.23 | 256 MB | 1 GB | | Debian | 9, 10, 11, 12, 13 | 256 MB | 1 ~ 1.5 GB ^ | | Kali | 滚动 | 256 MB | 1 ~ 1.5 GB ^ | | Ubuntu | 16.04 LTS - 24.04 LTS, 25.10 | 512 MB \* | 2 GB | | Anolis | 7, 8, 23 | 512 MB \* | 5 GB | | RHEL   AlmaLinux   Rocky   Oracle | 8, 9, 10 | 512 MB \* | 5 GB | | OpenCloudOS | 8, 9, Stream 23 | 512 MB \* | 5 GB | | CentOS Stream | 9, 10 | 512 MB \* | 5 GB | | Fedora | 42, 43 | 512 MB \* | 5 GB | | openEuler | 20.03 LTS - 24.03 LTS, 25.09 | 512 MB \* | 5 GB | | openSUSE | Leap 15.6, 16.0, Tumbleweed (滚动) | 512 MB \* | 5 GB | | NixOS | 25.11 | 512 MB | 5 GB | | Arch | 滚动 | 512 MB | 5 GB | | Gentoo | 滚动 | 512 MB | 5 GB | | 安同 OS | 滚动 | 512 MB | 5 GB | | 飞牛 fnOS | 1 | 512 MB | 8 GB | | Windows (DD) | 任何 | 512 MB | 取决于镜像 | | Windows (ISO) | Vista, 7, 8.x (Server 2008 - 2012 R2) | 512 MB | 25 GB | | Windows (ISO) | 10, 11 (Server 2016 - 2025) | 1 GB | 25 GB | \* 表示使用云镜像安装,非传统网络安装 ^ 表示需要 256 MB 内存 + 1.5 GB 硬盘,或 512 MB 内存 + 1 GB 硬盘 > [!WARNING] > > 本脚本理论上支持独服和 PC > > 但如果能使用 IPMI 或 U 盘,则不建议使用本脚本 > [!WARNING] > > ❌ 本脚本不支持 OpenVZ、LXC 虚拟机 > > 请改用 ## 下载(当前系统是 Linux) 国外服务器: ```bash curl -O https://raw.githubusercontent.com/bin456789/reinstall/main/reinstall.sh || wget -O ${_##*/} $_ ``` 国内服务器: ```bash curl -O https://cnb.cool/bin456789/reinstall/-/git/raw/main/reinstall.sh || wget -O ${_##*/} $_ ``` ## 下载(当前系统是 Windows) > [!IMPORTANT] > 请先关闭 `Windows Defender` 的 `实时保护` 功能。该功能会阻止 `certutil` 下载任何文件。
解决 Windows 7 下无法下载脚本 由于不支持 TLS 1.2、SHA-256、根证书没有更新等原因,Vista,7 和 Server 2008 (R2) 可能无法自动下载脚本,因此需要手动下载,具体操作如下: 用 IE 下载 (先在 IE 高级设置里启用 TLS 1.2),或者通过远程桌面,将这两个文件保存到同一个目录 - - 使用时运行下载的 `reinstall.bat`
国外服务器: ```batch certutil -urlcache -f -split https://raw.githubusercontent.com/bin456789/reinstall/main/reinstall.bat ``` 国内服务器: ```batch certutil -urlcache -f -split https://cnb.cool/bin456789/reinstall/-/git/raw/main/reinstall.bat ``` ## 使用 **所有功能** 都可在 Linux / Windows 下运行 - Linux 下运行 `bash reinstall.sh ...` - Windows 下先运行 `cmd`,再运行 `reinstall.bat ...` - 如果参数中的链接包含特殊字符,要用 `""` 将链接包裹起来,不能用 `''` ### 功能 1: 安装 Linux > [!CAUTION] > > 此功能会清除当前系统**整个硬盘**的全部数据(包含其它分区)! > > 如果不小心运行了脚本,可以在重启前运行 `bash reinstall.sh reset` 取消重装 - 用户名为 `root`,脚本会提示输入密码,不输入则使用随机密码 - 安装最新版可不输入版本号 - 最大化利用磁盘空间:不含 boot 分区(Fedora 例外),不含 swap 分区 - 自动根据机器类型选择不同的优化内核,例如 `Cloud`、`HWE` 内核 - 安装 Red Hat 时需填写 得到的 `qcow2` 镜像链接,也可以安装其它类 RHEL 系统的 `qcow2`,例如 `Alibaba Cloud Linux` 和 `TencentOS Server` - 重装后如需修改 SSH 端口或者改成密钥登录,注意还要修改 `/etc/ssh/sshd_config.d/` 里面的文件 ```bash bash reinstall.sh anolis 7|8|23 rocky 8|9|10 oracle 8|9|10 almalinux 8|9|10 opencloudos 8|9|23 centos 9|10 fnos 1 nixos 25.11 fedora 42|43 debian 9|10|11|12|13 alpine 3.20|3.21|3.22|3.23 opensuse 15.6|16.0|tumbleweed openeuler 20.03|22.03|24.03|25.09 ubuntu 16.04|18.04|20.04|22.04|24.04|25.10 [--minimal] kali arch gentoo aosc redhat --img="http://access.cdn.redhat.com/xxx.qcow2" ``` #### 可选参数 - `--password PASSWORD` 设置密码 - `--ssh-key KEY` 设置 SSH 登录公钥,[格式如下](#--ssh-key)。当使用公钥时,密码为空 - `--ssh-port PORT` 修改 SSH 端口(安装期间观察日志用,也作用于新系统) - `--web-port PORT` 修改 Web 端口(安装期间观察日志用) - `--frpc-config PATH` 添加 frpc 内网穿透,参数填配置文件的本地路径或 HTTP 链接 - `--hold 1` 仅重启到安装环境,不运行安装,用于 SSH 登录验证网络连通性 - `--hold 2` 安装结束后不重启,用于 SSH 登录修改系统内容,Debian/Kali 会挂载在 `/target`,其它系统会挂载在 `/os` > [!TIP] > > 可通过多种方式(SSH、HTTP 80 端口、商家后台 VNC、串行控制台)查看安装进度。 > > 即使安装过程出错,也能连接 SSH 手动救砖。 > > 目标系统非 Debian/Kali 时,可以运行 `/trans.sh alpine` 自动救砖成 Alpine 系统。
实验性功能 云镜像安装 Debian - 适合于 CPU 较慢的机器 ```bash bash reinstall.sh debian --ci ``` ISO 安装 CentOS, AlmaLinux, Rocky, Fedora - 仅支持内存大于 2G 且为动态 IP 的机器 - 密码 `123@@@`,SSH 端口 `22`,不支持用参数修改 ```bash bash reinstall.sh centos --installer ``` ISO 安装 Ubuntu - 仅支持内存大于 1G 且为动态 IP 的机器 - 密码 `123@@@`,SSH 端口 `22`,不支持用参数修改 ```bash bash reinstall.sh ubuntu --installer ```
### 功能 2: DD RAW 镜像到硬盘 > [!CAUTION] > > 此功能会清除当前系统**整个硬盘**的全部数据(包含其它分区)! > > 如果不小心运行了脚本,可以在重启前运行 `bash reinstall.sh reset` 取消重装 - 支持 `raw` 和固定大小的 `vhd` 镜像。未压缩或者压缩成 `.gz` `.xz` `.zst` `.tar` `.tar.gz` `.tar.xz` `.tar.zst` - DD Windows 镜像时,会自动扩展系统盘,静态 IP 的机器会配置好 IP,可能首次开机几分钟后才生效 - DD Linux 镜像时,**不会**修改镜像的任何内容 ```bash bash reinstall.sh dd --img "https://example.com/xxx.xz" ``` #### 可选参数 - `--allow-ping` 设置 Windows 防火墙允许被 Ping (仅限 DD Windows) - `--rdp-port PORT` 修改 RDP 端口 (仅限 DD Windows) - `--ssh-port PORT` 修改 SSH 端口(安装期间观察日志用) - `--web-port PORT` 修改 Web 端口(安装期间观察日志用) - `--frpc-config PATH` 添加 frpc 内网穿透(仅限 DD Windows),参数填配置文件的本地路径或 HTTP 链接 - `--cloud-data PATH_OR_URL` 为 DD Linux 镜像注入 cloud-init NoCloud 配置(仅限 DD Linux) - `--hold 1` 仅重启到安装环境,不运行安装,用于 SSH 登录验证网络连通性 - `--hold 2` DD 结束后不重启,用于 SSH 登录修改系统内容,Windows 系统会挂载在 `/os`,Linux 系统**不会**自动挂载 > [!TIP] > > `--cloud-data` 参数为本地目录或 HTTP 基础 URL,目录须包含 `user-data` 文件,`meta-data`、`network-config` 可选: > > ``` > seed/ > ├── user-data # 必须 > ├── meta-data # 可选 > └── network-config # 可选 > ``` > > ```bash > # 使用本地目录 > bash reinstall.sh dd --img "https://example.com/xxx.xz" --cloud-data /path/to/seed/ > # 使用 HTTP 目录 > bash reinstall.sh dd --img "https://example.com/xxx.xz" --cloud-data "https://example.com/seed/" > ``` > [!TIP] > > 可通过多种方式(SSH、HTTP 80 端口、商家后台 VNC、串行控制台)查看安装进度。 > > 即使安装过程出错,也能连接 SSH 手动救砖 > > 也可以运行 `/trans.sh alpine` 自动救砖成 Alpine 系统。 ### 功能 3: 重启到 Alpine Live OS(内存系统) - 可用 ssh 连接,进行备份/恢复硬盘、手动 DD、修改分区、手动安装 Alpine 等操作 - 用户名为 `root`,脚本会提示输入密码,不输入则使用随机密码 > [!TIP] > > 虽然运行的脚本叫 `reinstall`,但是此功能**不会**删除任何数据和进行自动重装,而是要用户手动操作 > > 如果用户手动操作没有破坏原系统,再次重启将回到原系统 ```bash bash reinstall.sh alpine --hold 1 ``` #### 可选参数 - `--password PASSWORD` 设置密码 - `--ssh-port PORT` 修改 SSH 端口 - `--ssh-key KEY` 设置 SSH 登录公钥,[格式如下](#--ssh-key)。当使用公钥时,密码为空 - `--frpc-config PATH` 添加 frpc 内网穿透,参数填配置文件的本地路径或 HTTP 链接 ### 功能 4: 重启到 netboot.xyz - 可使用商家后台 VNC 手动安装 [更多系统](https://github.com/netbootxyz/netboot.xyz?tab=readme-ov-file#what-operating-systems-are-currently-available-on-netbootxyz) > [!TIP] > > 虽然运行的脚本叫 `reinstall`,但是此功能**不会**删除任何数据和进行自动重装,而是要用户手动操作 > > 如果用户手动操作没有破坏原系统,再次重启将回到原系统 ```bash bash reinstall.sh netboot.xyz ``` ![netboot.xyz](https://netboot.xyz/images/netboot.xyz.gif) ### 功能 5: 安装 Windows ISO ![Windows 安装界面](https://github.com/bin456789/reinstall/assets/7548515/07c1aea2-1ce3-4967-904f-aaf9d6eec3f7) > [!CAUTION] > > 此功能会清除当前系统**整个硬盘**的全部数据(包含其它分区)! > > 如果不小心运行了脚本,可以在重启前运行 `bash reinstall.sh reset` 取消重装 - 用户名为 `administrator`,脚本会提示输入密码,不输入则使用随机密码 - 如果远程登录失败,可以尝试使用用户名 `.\administrator` - 静态机器会自动配置好 IP,可能首次开机几分钟后才生效 - 支持任意语言的 ISO - 支持绕过 Windows 11 硬件限制 #### 支持的系统 - Windows (Vista ~ 11) - Windows Server (2008 ~ 2025) - Windows Server Essentials - Windows Server (Semi) Annual Channel - Hyper-V Server - Azure Local (Azure Stack HCI) #### 方法 1: 让脚本自动查找 ISO - 脚本会从 查找 ISO,该网站专门提供官方 ISO 下载 - 只支持查找 Windows 10, 11, Server 2019, 2022, 2025 的 ISO ```bash bash reinstall.sh windows \ --image-name "Windows 11 Enterprise LTSC 2024" \ --lang zh-cn ```
支持的语言 ```text ar-sa bg-bg cs-cz da-dk de-de el-gr en-gb en-us es-es es-mx et-ee fi-fi fr-ca fr-fr he-il hr-hr hu-hu it-it ja-jp ko-kr lt-lt lv-lv nb-no nl-nl pl-pl pt-pt pt-br ro-ro ru-ru sk-sk sl-si sr-latn-rs sv-se th-th tr-tr uk-ua zh-cn zh-hk zh-tw ```
#### 方法 2: 自行指定 ISO 连接 - 如果不知道 `--image-name`,可以随便填,在重启后连接 SSH,根据错误提示重新输入正确的值 ```bash bash reinstall.sh windows \ --image-name "Windows 11 Enterprise LTSC 2024 Evaluation" \ --iso "https://go.microsoft.com/fwlink/?linkid=2289029" ``` 或者磁力链接 ```bash bash reinstall.sh windows \ --image-name "Windows 11 Enterprise LTSC 2024" \ --iso "magnet:?xt=urn:btih:7352bd2db48c3381dffa783763dc75aa4a6f1cff" ```
以下网站可找到 ISO 链接 - 正式版 - - - - - (需用非 Windows User-Agent 打开) - - - 评估版 - - - - - - - - Insider 预览版 - -
#### 可选参数 - `--password PASSWORD` 设置密码 - `--allow-ping` 设置 Windows 防火墙允许被 Ping - `--rdp-port PORT` 更改 RDP 端口 - `--ssh-port PORT` 修改 SSH 端口(仅安装期间观察日志用) - `--web-port PORT` 修改 Web 端口(仅安装期间观察日志用) - `--add-driver INF_OR_DIR` 添加额外驱动,填写 .inf 路径,或者 .inf 所在的文件夹 - 需先下载驱动到当前系统 - 可多次设置该参数以添加不同的驱动 - `--frpc-config PATH` 添加 frpc 内网穿透,参数填配置文件的本地路径或 HTTP 链接 - `--hold 1` 仅重启到安装环境,不运行安装,用于 SSH 登录验证网络连通性 - `--hold 2` 用于在进入 Windows 官方安装程序之前,SSH 登录修改 `boot.wim`、`install.wim` 或者其它内容,硬盘挂载在 `/os` #### 以下驱动会自动按需下载安装,无需手动添加 - VirtIO ([社区版][virtio-virtio], [阿里云][virtio-aliyun], [腾讯云][virtio-qcloud], [GCP][virtio-gcp]) - XEN ([~~社区版~~][xen-xen] (未签名), [Citrix][xen-citrix], [AWS][xen-aws]) - AWS ([ENA 网卡][aws-ena], [NVME 存储控制器][aws-nvme]) - GCP ([gVNIC 网卡][gcp-gvnic], [GGA 显卡][gcp-gga]) - Azure ([MANA 网卡][azure-mana]) - Intel (VMD 存储控制器: [11代酷睿][intel-vmd-gen11], [12-15代酷睿][intel-vmd-gen12-to-gen15], 网卡: [7][intel-nic-7], [8.x][intel-nic-8.1], [10][intel-nic-10], [11][intel-nic-11], [2008 R2][intel-nic-7], [2012][intel-nic-2012], [2012 R2][intel-nic-2012-r2], [2016][intel-nic-2016], [2019][intel-nic-2019], [2022][intel-nic-2022], [2025][intel-nic-2025]) [virtio-virtio]: https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/ [virtio-aliyun]: https://www.alibabacloud.com/help/ecs/user-guide/install-the-virtio-driver-1 [virtio-qcloud]: https://cloud.tencent.com/document/product/213/17815#b84b2032-752c-43c4-a509-73530b8f82ff [virtio-gcp]: https://console.cloud.google.com/storage/browser/gce-windows-drivers-public [xen-xen]: https://xenproject.org/resources/downloads/ [xen-aws]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/xen-drivers-overview.html [xen-citrix]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Upgrading_PV_drivers.html#win2008-citrix-upgrade [aws-ena]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ena-driver-releases-windows.html [aws-nvme]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/nvme-driver-version-history.html [gcp-gvnic]: https://cloud.google.com/compute/docs/networking/using-gvnic [gcp-gga]: https://cloud.google.com/compute/docs/instances/enable-instance-virtual-display [azure-mana]: https://learn.microsoft.com/azure/virtual-network/accelerated-networking-mana-windows [intel-vmd-gen11]: https://www.intel.com/content/www/us/en/download/849933/intel-rapid-storage-technology-driver-installation-software-with-intel-optane-memory-12th-to-13th-gen-platforms.html [intel-vmd-gen12-to-gen15]: https://www.intel.com/content/www/us/en/download/849936/intel-rapid-storage-technology-driver-installation-software-with-intel-optane-memory-12th-to-15th-gen-platforms.html [intel-nic-7]: https://www.intel.com/content/www/us/en/download/15590/intel-network-adapter-driver-for-windows-7-final-release.html [intel-nic-8.1]: https://www.intel.com/content/www/us/en/download/17479/intel-network-adapter-driver-for-windows-8-1.html [intel-nic-10]: https://www.intel.com/content/www/us/en/download/18293/intel-network-adapter-driver-for-windows-10.html [intel-nic-11]: https://www.intel.com/content/www/us/en/download/727998/intel-network-adapter-driver-for-microsoft-windows-11.html [intel-nic-2012]: https://www.intel.com/content/www/us/en/download/16789/intel-network-adapter-driver-for-windows-server-2012.html [intel-nic-2012-r2]: https://www.intel.com/content/www/us/en/download/17480/intel-network-adapter-driver-for-windows-server-2012-r2.html [intel-nic-2016]: https://www.intel.com/content/www/us/en/download/18737/intel-network-adapter-driver-for-windows-server-2016.html [intel-nic-2019]: https://www.intel.com/content/www/us/en/download/19372/intel-network-adapter-driver-for-windows-server-2019.html [intel-nic-2022]: https://www.intel.com/content/www/us/en/download/706171/intel-network-adapter-driver-for-windows-server-2022.html [intel-nic-2025]: https://www.intel.com/content/www/us/en/download/838943/intel-network-adapter-driver-for-windows-server-2025.html #### 如何填写映像名称 `--image-name` 一个 ISO 通常包含多个系统版本,例如家庭版、专业版。因此需要用 `--image-name` 指定要安装的系统版本(映像名称),不区分大小写 可以用 DISM、DISM++、Wimlib 等工具查询 ISO 包含的映像名称 常用的映像名称有: ```text Windows 7 Ultimate Windows 11 Pro Windows 11 Enterprise LTSC 2024 Windows Server 2025 SERVERDATACENTER ``` #### 如何用 [DISM++](https://github.com/Chuyu-Team/Dism-Multi-language/releases) 查询 ISO 包含的映像名称 打开文件菜单 > 打开映像文件,选择要安装的 iso,即可得到映像名称,所有映像名称都可以安装 ![image-name](https://github.com/bin456789/reinstall/assets/7548515/5aae0a9b-61e2-4f66-bb98-d470a6beaac2) > [!WARNING] > Vista (Server 2008) 和 32 位系统可能会缺少驱动 > [!WARNING] > > 安装 Windows 7 (Server 2008 R2) 时 > > 1. EFI 引导的机器要开启 CSM > > 2. Hyper-V (Azure) 需选择第 1 代虚拟机 > [!WARNING] > > Windows 10 LTSC 2021 中文版镜像 `zh-cn_windows_10_enterprise_ltsc_2021_x64_dvd_033b7312.iso` 的 `wsappx` 进程会长期占用 CPU > > 解决方法是更新系统补丁,或者手动安装 `VCLibs` 库 > [!WARNING] > > 在 GCP 上安装 `2022年5月` 和之后发布的 Windows ISO,在引导 Windows 安装界面 (PE) 时会不断反复重启。解决方法如下,二选一 > > 1. 添加 `--force-boot-mode bios` 参数,脚本将以 `BIOS 引导 + MBR 分区表` 方式安装 Windows > > (可选) 安装完成后用 `MBR2GPT /convert /allowFullOS` 命令转为 `EFI 引导 + GPT 分区表` > > 2. 自制 RAW 镜像并通过 DD 安装 #### ARM 安装 Windows 的注意事项 大部分 ARM 机器都支持安装最新版 Windows 11 安装过程可能会黑屏,串行控制台可能会显示 `ConvertPages: failed to find range`,均不影响正常安装 | 兼容性 | 云服务商 | 实例类型 | 问题 | | ------ | -------- | ----------------------- | ------------------------------------------ | | ✔️ | Azure | B2pts_v2 | | | ✔️ | AWS | T4g | | | ✔️ | Scaleway | COPARM1 | | | ✔️ | Gcore | | | | ❔ | 阿里云 | g6r, c6r, g8y, c8y, r8y | 有几率重启时卡开机 Logo,强制重启即可 | | ❔ | 甲骨文云 | A1.Flex | 不一定能安装成功,越新创建的实例越容易成功 | | ❌ | 谷歌云 | t2a | 缺少网卡驱动 | ### 取消重装 - 如果不小心运行了脚本,可以运行以下命令取消重装 - 需要在重启前运行 ```bash bash reinstall.sh reset ``` ## 参数格式 ### --ssh-key - `--ssh-key "ssh-rsa ..."` - `--ssh-key "ssh-ed25519 ..."` - `--ssh-key "ecdsa-sha2-nistp256/384/521 ..."` - `--ssh-key http://path/to/public_key` - `--ssh-key github:your_username` - `--ssh-key gitlab:your_username` - `--ssh-key /path/to/public_key` - `--ssh-key C:\path\to\public_key` ## 如何使用旧版本 根据 Bug 守恒定律,修复旧 Bug 的同时会引入新的 Bug 如果遇到新的 Bug,可以试下旧版本是否正常 从 右侧找到旧版本的 `commit_id` ```bash commit_id=xxxxxxx curl -O https://raw.githubusercontent.com/bin456789/reinstall/$commit_id/reinstall.sh || wget -O ${_##*/} $_ sed -i "/^confhome.*main$/s/main/$commit_id/" reinstall.sh bash reinstall.sh ... ``` ## 如何修改脚本自用 1. Fork 本仓库 2. 修改 `reinstall.sh` 和 `reinstall.bat` 开头的 `confhome` 和 `confhome_cn` 3. 修改其它代码 ## 感谢 感谢以下商家提供白嫖机器 [![Oracle Cloud](https://github.com/bin456789/reinstall/assets/7548515/8b430ed4-8344-4f96-b4da-c2bda031cc90)](https://www.oracle.com/cloud/) [![DartNode](https://github.com/bin456789/reinstall/assets/7548515/435d6740-bcdd-4f3a-a196-2f60ae397f17)](https://dartnode.com/) ================================================ FILE: cloud-init-fix-onlink.sh ================================================ #!/bin/bash # 修复 cloud-init 没有正确渲染 onlink 网关 set -eE os_dir=$1 # 该脚本也会在 alpine live 下调用 # 防止在 alpine live 下运行 systemctl netplan 报错 systemctl() { if systemd-detect-virt --chroot; then return fi command systemctl "$@" } netplan() { if systemd-detect-virt --chroot; then return fi command netplan "$@" } insert_into_file() { file=$1 location=$2 regex_to_find=$3 if [ "$location" = head ]; then bak=$(mktemp) cp "$file" "$bak" cat - "$bak" >"$file" else line_num=$(grep -E -n "$regex_to_find" "$file" | cut -d: -f1) found_count=$(echo "$line_num" | wc -l) if [ ! "$found_count" -eq 1 ]; then return 1 fi case "$location" in before) line_num=$((line_num - 1)) ;; after) ;; *) return 1 ;; esac sed -i "${line_num}r /dev/stdin" "$file" fi } fix_netplan_conf() { # 修改前 # gateway4: 1.1.1.1 # gateway6: ::1 # 修改后 # routes: # - to: 0.0.0.0/0 # via: 1.1.1.1 # on-link: true # routes: # - to: ::/0 # via: ::1 # on-link: true conf=$os_dir/etc/netplan/50-cloud-init.yaml if ! [ -f "$conf" ]; then return fi # 判断 bug 是否已经修复 if grep -q 'on-link:' "$conf"; then return fi # 获取网关 gateways=$(grep 'gateway[4|6]:' "$conf" | awk '{print $2}') if [ -z "$gateways" ]; then return fi # 获取缩进 spaces=$(grep 'gateway[4|6]:' "$conf" | head -1 | grep -o '^[[:space:]]*') { # 网关头部 cat </dev/null); then return fi for conf in $confs; do # 判断 bug 是否已经修复 if grep -q '^GatewayOnLink=' "$conf"; then return fi # 获取网关 gateways=$(grep '^Gateway=' "$conf" | cut -d= -f2) if [ -z "$gateways" ]; then return fi # 删除原来的条目 sed -i '/^\[Route\]/d; /^Gateway=/d; /^GatewayOnLink=/d' "$conf" # 创建新条目 for gateway in $gateways; do echo " [Route] Gateway=$gateway GatewayOnLink=yes " done >>"$conf" done # 重新应用配置 # networkctl reload 不起作用 if systemctl -q is-enabled systemd-networkd; then systemctl restart systemd-networkd fi } fix_wicked_conf() { # https://github.com/openSUSE/wicked/wiki/FAQ#q-why-wicked-does-not-set-my-default-static-route # 修改前 # default 1.1.1.1 - - # default 2602::1 - - # 修改后 # 1.1.1.1 - - # 2602::1 - - # default 1.1.1.1 - - # default 2602::1 - - if ! confs=$(ls "$os_dir/etc/sysconfig/network/ifroute-"* 2>/dev/null); then return fi for conf in $confs; do # 判断 bug 是否已经修复 if grep -v 'default' "$conf" | grep -q '-'; then return fi # 获取网关 gateways=$(awk '$1=="default" {print $2}' "$conf") if [ -z "$gateways" ]; then return fi # 创建新条目 for gateway in $gateways; do echo "$gateway - -" done | insert_into_file "$conf" head done # 重新应用配置 if systemctl -q is-enabled wicked; then systemctl restart wicked fi } # ubuntu 18.04 cloud-init 版本 23.1.2,因此不用处理 # debian 10/11 云镜像原本用 ifupdown + resolvconf,脚本改成用 netplan + networkd/resolved # debian 12 云镜像: netplan + networkd/resolved # 23.1.1 修复 fix_netplan_conf # arch: networkd/resolved # gentoo: networkd/resolved # 24.2 修复 # 只需对云镜像处理 # 因为普通安装用的是 alpine 的 cloud-init,版本够新,不用处理 fix_networkd_conf # opensuse 15.5: ifcfg + netconfig (dns) + wicked fix_wicked_conf ================================================ FILE: cloud-init.yaml ================================================ #cloud-config datasource_list: [None] timezone: Asia/Shanghai disable_root: false ssh_pwauth: true users: - name: root lock_passwd: false chpasswd: expire: false # <= cloud-init 22.2.x 需要 list: | root:@PASSWORD@ users: - name: root password: "@PASSWORD@" type: hash runcmd: # opensuse tumbleweed 镜像有 /etc/ssh/sshd_config.d/ 文件夹,没有 /etc/ssh/sshd_config,有/usr/etc/ssh/sshd_config # opensuse tumbleweed cloud-init 直接创建并写入 /etc/ssh/sshd_config,造成默认配置丢失 # 下面这行删除 clout-init 创建的 sshd_config - test $(wc -l >/etc/ssh/sshd_config.d/50-cloud-init.conf && rm -f /etc/ssh/sshd_config - echo "PermitRootLogin yes" >/etc/ssh/sshd_config.d/01-permitrootlogin.conf 2>/dev/null || sed -Ei 's/^#?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config - echo "Port @SSH_PORT@" >/etc/ssh/sshd_config.d/01-change-ssh-port.conf || sed -Ei 's/^#?Port .*/Port @SSH_PORT@/' /etc/ssh/sshd_config # 已创建的 ssh 连接会沿用旧的配置(未开启密码登录),这时即使输入正确的密码,也会提示 Access Denied # systemctl restart sshd 只会重启监听进程,不会关闭已创建的连接(子进程) - pkill sshd || true # daemon-reload 会刷新 /run/systemd/generator/ssh.socket.d/addresses.conf - systemctl daemon-reload - for s in ssh.socket ssh.service sshd.socket sshd.service; do systemctl is-enabled $s 2>/dev/null && systemctl restart $s && break; done # 删除有密码的行 - sed -i -e '/^[[:space:]]*password:/d' -e '/[[:space:]]*root:/d' /etc/cloud/cloud.cfg.d/99_fallback.cfg - touch /etc/cloud/cloud-init.disabled # ubuntu 镜像运行 echo -e '\nDone' ,-e 会被显示出来 # 加 true 因为有的 tty 不可写 - for tty in tty0 ttyS0 ttyAMA0; do [ -c /dev/$tty ] && printf '\n%s\n' 'reinstall done' >/dev/$tty || true; done ================================================ FILE: debian.cfg ================================================ #_preseed_V1 # shellcheck disable=SC1091,SC2148 # https://www.debian.org/releases/stable/amd64/apbs04.zh-cn.html # https://www.debian.org/releases/stable/example-preseed.txt # https://preseed.debian.net/debian-preseed/trixie/amd64-main-full.txt # 需要留意 kali initrd 自带的 /preseed.cfg # 下面这行语句无效,因为本行后面有反斜杠,前面有空格(安装器认为不算注释)\ d-i debian-installer/locale string en_US.UTF-8 # B.4.1. 本地化 d-i debian-installer/locale string en_US.UTF-8 d-i keyboard-configuration/xkb-keymap select us # B.4.2. 网络设置 d-i netcfg/get_hostname string unassigned-hostname d-i netcfg/get_domain string unassigned-domain d-i netcfg/hostname string localhost # B.4.3. 网络控制台 # B.4.4. 镜像设置 d-i mirror/country string manual # d-i mirror/http/hostname string deb.debian.org # B.4.5. 帐号设置 d-i passwd/make-user boolean false # 注意如果用 ssh key 后面还要删除密码 # d-i passwd/root-password password '' # d-i passwd/root-password-again password '' # d-i passwd/root-password-crypted password '' # kali 需要下面这行,否则会提示输入用户名 d-i passwd/root-login boolean true # B.4.6. 时钟与时区设置 d-i time/zone string Asia/Shanghai # B.4.7. 分区 d-i partman-auto/method string regular d-i partman-lvm/device_remove_lvm boolean true d-i partman-md/device_remove_md boolean true d-i partman-partitioning/confirm_write_new_label boolean true d-i partman/choose_partition select finish d-i partman/confirm boolean true d-i partman/confirm_nooverwrite boolean true # vm 原有系统是 bios + gpt,切换成 efi,用 iso 重装,需要确认此项 # 用脚本重装的话,强制安装在第二个硬盘上也可能会遇到? d-i partman-efi/non_efi_system boolean true ### Description: Do you want to return to the partitioning menu? # You have not selected any partitions for use as swap space. Enabling swap # space is recommended so that the system can make better use of the # available physical memory, and so that it behaves better when physical # memory is scarce. You may experience installation problems if you do not # have enough physical memory. # . # If you do not go back to the partitioning menu and assign a swap partition, # the installation will continue without swap space. # 坑的一比 # 不是确认是否 no_swap # 而是 recipe no_swap 时,确认是否返回上一级重新分区 # 选择 true 就一直死循环 d-i partman-basicfilesystems/no_swap boolean false # 分区大小计算 # https://salsa.debian.org/installer-team/partman-base/-/blob/master/lib/base.sh # 最小值 膨胀权重 最大值 # https://salsa.debian.org/installer-team/partman-auto/-/blob/master/recipes/atomic?ref_type=heads # https://salsa.debian.org/installer-team/partman-auto/-/blob/master/recipes-amd64-efi/atomic?ref_type=heads # shellcheck disable=SC1083,SC2086,SC2154 d-i partman-auto/expert_recipe_efi string efi :: \ 106 1 106 free \ $iflabel{ gpt } method{ efi } format{ } . \ 1 1 -1 $default_filesystem \ method{ format } format{ } use_filesystem{ } $default_filesystem{ } mountpoint{ / } . # 大于 2T 会自动用 gpt # shellcheck disable=SC1083,SC2086,SC2154 d-i partman-auto/expert_recipe_bios string bios :: \ 1 1 1 free \ $iflabel{ gpt } method{ biosgrub } . \ 1 1 -1 $default_filesystem \ method{ format } format{ } use_filesystem{ } $default_filesystem{ } mountpoint{ / } . # B.4.8. 基本系统安装 # B.4.9. 设置 apt d-i apt-setup/non-free boolean true d-i apt-setup/non-free-firmware boolean true d-i apt-setup/contrib boolean true d-i apt-setup/enable-source-repositories boolean false # kali 不要设置 # d-i apt-setup/security_host string security.debian.org # B.4.10. 选择软件包 tasksel tasksel/first multiselect ssh-server d-i pkgsel/upgrade select none # B.4.11. 安装 bootloader # 添加 bootx64.efi d-i grub-installer/force-efi-extra-removable boolean true # B.4.12. 完成安装 # 由下面的 hold 2 设置 # d-i finish-install/reboot_in_progress note # B.4.13. 预置其他的软件包 # 其他设置 # d-i anna/standard_modules boolean false # d-i anna/choose_modules string network-console # d-i network-console/password password '' # d-i network-console/password-again password '' # B.5.1. 安装过程中运行用户命令 # 注意所有命令都会合并成一行命令 # 最后的 true; \ 没什么用,只是让 vscode 代码高亮不报错误 # debian 11+ 才有 websocketd # 有 /cdrom/simple-cdd 才安装 simple-cdd-profiles # 不然安装时 control 脚本会报错: # Loading simple-cdd-profiles failed for unknown reasons # 未下载的组件,无法用 debconf-set,需要用 debconf-set-selections # https://salsa.debian.org/installer-team/network-console/-/blob/master/debian/network-console.postinst?ref_type=heads # https://salsa.debian.org/installer-team/user-setup/-/blob/master/user-setup-apply?ref_type=heads # 此时还没有配置源,anna-install 会在配置完源后再安装 d-i preseed/early_command string true; \ for str in $(grep -wo "extra_[^ ]*" /proc/cmdline | sed 's/^extra_//'); do eval "$str"; done; \ di(){ \ echo "d-i $*" >/tmp/selections.cfg; \ echo "d-i $*" >>/tmp/selections.cfg.all; \ debconf-set-selections /tmp/selections.cfg; \ rm -f /tmp/selections.cfg; \ }; \ run_as_service_with_screen() { \ if ! [ -f /etc/screenrc.bak ]; then \ cp /etc/screenrc /etc/screenrc.bak; \ fi; \ true >/etc/screenrc; \ screen sh -c 'while true; do pidof ${1##*/} || "$@"; sleep 5; done' _ "$@"; \ cp -f /etc/screenrc.bak /etc/screenrc; \ }; \ if [ "$hold" = 1 ]; then \ di auto-install/enable boolean false; \ di debconf/priority select low; \ di partman/early_command string; \ else \ { \ echo 'Reinstalling...'; \ echo 'Option 1. View logs:'; \ echo ' tail -fn+1 /var/log/syslog'; \ echo 'Option 2. Attach to the installer:'; \ echo ' TERM=screen screen -xp1'; \ } >>/etc/motd; \ mem=$(grep ^MemTotal: /proc/meminfo | { read -r _ y _; echo "$((y / 1024))"; }); \ if command -v websocketd && [ "$mem" -ge 400 ]; then \ for _ in {1..10}; do \ if wget "$confhome/logviewer.html" -O /tmp/index.html; then \ break; \ fi; \ sleep 5; \ done; \ if [ -z "$web_port" ]; then \ web_port=80; \ fi; \ run_as_service_with_screen websocketd --port 80 --loglevel=fatal --staticdir=/tmp \ sh -c "tail -fn+0 /var/log/syslog | tr '\r' '\n' | grep -Fiv -e password -e token" ; \ fi; \ fi; \ if ! [ "$hold" = 2 ]; then \ di finish-install/reboot_in_progress note; \ fi; \ if [ -s /configs/ssh_keys ]; then \ di passwd/root-password-crypted password "''"; \ else \ di passwd/root-password-crypted password "$(cat /configs/password-linux-sha512)"; \ fi; \ mkdir -p /etc/ssh; \ true >/etc/ssh/sshd_config; \ if [ -s /configs/ssh_keys ]; then \ (umask 077; mkdir -p /.ssh; cat /configs/ssh_keys >/.ssh/authorized_keys); \ else \ echo "PermitRootLogin yes" >>/etc/ssh/sshd_config; \ fi; \ if [ -n "$ssh_port" ] && ! [ "$ssh_port" = 22 ]; then \ echo "Port $ssh_port" >>/etc/ssh/sshd_config; \ fi; \ grep -qs ^root: /etc/shadow || echo "root:$(cat /configs/password-linux-sha512):1:0:99999:7:::" >>/etc/shadow; \ grep -qs ^nogroup: /etc/group || echo "nogroup:*:65534:" >>/etc/group; \ grep -qs ^sshd: /etc/passwd || echo "sshd:*:100:65534::/run/sshd:/bin/false" >>/etc/passwd; \ mkdir -p /run/sshd; \ chmod 0755 /run/sshd; \ ssh-keygen -A; \ run_as_service_with_screen /usr/sbin/sshd -D; \ if ls /configs/frpc.* >/dev/null 2>&1; then \ url=$(sh /get-frpc-url.sh linux); \ mkdir -p /usr/local/bin; \ mkdir -p /usr/local/etc/frpc; \ for _ in {1..10}; do \ if wget -O- "$url" | tar xz "*/frpc" -O >/usr/local/bin/frpc; then \ break; \ fi; \ sleep 5; \ done; \ chmod a+x /usr/local/bin/frpc; \ cp /configs/frpc.* /usr/local/etc/frpc/; \ run_as_service_with_screen /usr/local/bin/frpc -c /usr/local/etc/frpc/frpc.*; \ fi; \ if [ -d /cdrom/simple-cdd ]; then \ anna-install simple-cdd-profiles; \ fi # debian 11 initrd 没有 xargs awk # debian 12 initrd 没有 xargs # efi 分区大小未改变时,不会被格式化,因此需要手动删除旧系统的 efi 文件 # os-prober 卡太久,因此跳过 d-i partman/early_command string true; \ eval "$(grep -o 'extra_confhome=[^ ]*' /proc/cmdline | sed 's/^extra_//')"; \ postinst=/var/lib/dpkg/info/bootstrap-base.postinst; \ cp $postinst $postinst.orig; \ true >$postinst; \ swapfile=/target/swapfile; \ mem=$(grep ^MemTotal: /proc/meminfo | { read -r _ y _; echo "$((y / 1024))"; }); \ swap_size=$((512 - mem)); \ if [ $swap_size -gt 0 ]; then \ echo "fallocate -l ${swap_size}M $swapfile; mkswap $swapfile; swapon $swapfile" >>$postinst; \ fi; \ echo "swapoff -a; rm -f $swapfile" >/usr/lib/finish-install.d/95swapoff; \ chmod a+x /usr/lib/finish-install.d/95swapoff; \ echo "rm -rf /target/boot/efi/*; $postinst.orig" >>$postinst; \ xda=$(sh /get-xda.sh); \ debconf-set partman-auto/disk "/dev/$xda"; \ debconf-set grub-installer/bootdev "/dev/$xda"; \ rm -rf /usr/sbin/fdisk /usr/sbin/sfdisk; \ ttys=$(sh /ttys.sh console=); \ debconf-set debian-installer/add-kernel-opts "$ttys"; \ eths=$(cd /dev/netconf/ && ls); \ if ! sh /can_use_cloud_kernel.sh "$xda" $eths; then \ debconf-set base-installer/kernel/image "$(debconf-get base-installer/kernel/image | sed 's/-cloud//')"; \ fi; \ if [ -d /sys/firmware/efi ]; then \ debconf-set partman-auto/expert_recipe "$(debconf-get partman-auto/expert_recipe_efi)"; \ else \ debconf-set partman-auto/expert_recipe "$(debconf-get partman-auto/expert_recipe_bios)"; \ fi; \ true >/bin/os-prober # kali ssh 默认关闭 # 另一种方法处理 cloudcone # if [ "$link_grub_dir" = 1 ]; then mkdir /target/boot/grub2; echo 'chainloader (hd0)+1' >/target/boot/grub2/grub.cfg; fi; \ # debian 9 tar 不支持 --strip-components d-i preseed/late_command string true; \ for str in $(grep -wo "extra_[^ ]*" /proc/cmdline | sed 's/^extra_//'); do eval "$str"; done; \ if [ "$elts" = 1 ]; then sed -i "s|deb\.freexian\.com/extended-lts|$deb_mirror|" /target/etc/apt/sources.list; fi; \ if [ "$link_grub_dir" = 1 ]; then ln -s grub /target/boot/grub2; fi; \ in-target systemctl enable ssh; \ if [ -s /configs/ssh_keys ]; then \ (umask 077; mkdir -p /target/root/.ssh; cat /configs/ssh_keys >/target/root/.ssh/authorized_keys); \ in-target passwd -d root; \ else \ echo "PermitRootLogin yes" >/target/etc/ssh/sshd_config.d/01-permitrootlogin.conf || \ echo "PermitRootLogin yes" >>/target/etc/ssh/sshd_config; \ fi; \ if [ -n "$ssh_port" ] && ! [ "$ssh_port" = 22 ]; then \ echo "Port $ssh_port" >/target/etc/ssh/sshd_config.d/01-change-ssh-port.conf || \ echo "Port $ssh_port" >>/target/etc/ssh/sshd_config; \ fi; \ if ls /configs/frpc.* >/dev/null 2>&1; then \ mkdir -p /target/usr/local/bin; \ mkdir -p /target/usr/local/etc/frpc; \ cp /usr/local/bin/frpc /target/usr/local/bin/; \ cp /usr/local/etc/frpc/frpc.* /target/usr/local/etc/frpc/; \ chmod a+x /target/usr/local/bin/frpc; \ cp /frpc.service /target/etc/systemd/system/; \ in-target systemctl enable frpc; \ fi; \ cp /fix-eth-name.sh /target/; \ cp /fix-eth-name.service /target/etc/systemd/system/; \ in-target systemctl enable fix-eth-name ================================================ FILE: fix-eth-name.initd ================================================ #!/sbin/openrc-run Description="Fix Eth Name" # https://gitlab.alpinelinux.org/alpine/aports/-/blob/master/main/openrc/networking.initd # https://gitlab.alpinelinux.org/alpine/aports/-/blob/master/main/dhcpcd/dhcpcd.initd depend() { need localmount want dev-settle after bootmisc hwdrivers modules before net networking dhcpcd } start() { ebegin "Fix Eth Name" ash /fix-eth-name.sh eend $? } start_post() { rc-service fix-eth-name zap rc-update del fix-eth-name boot rm -f /etc/init.d/fix-eth-name rm -f /fix-eth-name.sh } ================================================ FILE: fix-eth-name.service ================================================ [Unit] Description=Fix Eth Name ConditionPathExists=/fix-eth-name.sh After=dbus.service Before=cloud-init-local.service Before=network.service Before=networking.service Before=systemd-networkd.service Before=NetworkManager.service Before=wickedd-auto4.service Before=wickedd-dhcp4.service Before=wickedd-dhcp6.service Before=wickedd.service Before=network.target [Service] Type=oneshot ExecStart=/usr/bin/env bash /fix-eth-name.sh ExecStart=/usr/bin/env rm -f /fix-eth-name.sh ExecStart=/usr/bin/env rm -f /etc/systemd/system/fix-eth-name.service ExecStart=/usr/bin/env rm -f /etc/systemd/system/multi-user.target.wants/fix-eth-name.service ExecStart=/usr/bin/env rm -f /lib/systemd/system-preset/01-fix-eth-name.preset ExecStart=/usr/bin/env rm -f /usr/lib/systemd/system-preset/01-fix-eth-name.preset [Install] WantedBy=multi-user.target ================================================ FILE: fix-eth-name.sh ================================================ #!/usr/bin/env bash # shellcheck shell=dash # shellcheck disable=SC3001,SC3010 # alpine 使用 busybox ash set -eE # openeuler 需等待 udev 将网卡名从 eth0 改为 enp3s0 sleep 10 # 不知道有没有用 if command -v udevadm >/dev/null; then # udevadm trigger udevadm settle elif command -v mdev >/dev/null; then mdev -sf fi # 本脚本在首次进入新系统后运行 # 将 trans 阶段生成的网络配置中的网卡名(eth0) 改为正确的网卡名,也适用于以下情况 # 1. alpine 要运行此脚本,因为安装后的内核可能有 netboot 没有的驱动 # 2. dmit debian 普通内核(安装时)和云内核网卡名不一致 # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=928923 # todo: 删除 cloud-init to_lower() { tr '[:upper:]' '[:lower:]' } retry() { local max_try=$1 shift for i in $(seq "$max_try"); do if "$@"; then return else ret=$? if [ "$i" -ge "$max_try" ]; then return $ret fi sleep 1 fi done } # openeuler 本脚本运行一秒后才有 enp3s0 # 用 systemd-analyze plot >a.svg 发现 sys-subsystem-net-devices-enp3s0.device 也是出现在 NetworkManager 之后 # 因此需要等待网卡出现 get_ethx_by_mac() { retry 10 _get_ethx_by_mac "$@" } _get_ethx_by_mac() { mac=$(echo "$1" | to_lower) flag=$2 if [ -z "$flag" ]; then flag=master fi if true; then if [ "$flag" = master ]; then # master # 过滤 azure vf (带 master ethx) ip -o link | grep -i "$mac" | grep -v master | awk '{print $2}' | cut -d: -f1 | grep . else # slave # 带 master ethx ip -o link | grep -i "$mac" | grep -w master | awk '{print $2}' | cut -d: -f1 | grep . fi else for i in $(cd /sys/class/net && echo *); do if [ "$(cat "/sys/class/net/$i/address")" = "$mac" ]; then if [ $(($(cat "/sys/class/net/$i/flags") & 0x800)) -ne 0 ]; then fact_flag=slave else fact_flag=master fi if [ "$flag" = "$fact_flag" ]; then echo "$i" return fi fi done return 1 fi } fix_rh_sysconfig() { for file in /etc/sysconfig/network-scripts/ifcfg-eth*; do # 没有 ifcfg-eth* 也会执行一次,因此要判断文件是否存在 [ -f "$file" ] || continue mac=$(grep ^HWADDR= "$file" | cut -d= -f2 | grep .) || continue ethx=$(get_ethx_by_mac "$mac") || continue proper_file=/etc/sysconfig/network-scripts/ifcfg-$ethx if [ "$file" != "$proper_file" ]; then # 更改文件内容 sed -i "s/^DEVICE=.*/DEVICE=$ethx/" "$file" # 不要直接更改文件名,因为可能覆盖已有文件 mv "$file" "$proper_file.tmp" fi done # 更改文件名 for tmp_file in /etc/sysconfig/network-scripts/ifcfg-e*.tmp; do if [ -f "$tmp_file" ]; then mv "$tmp_file" "${tmp_file%.tmp}" fi done } fix_suse_sysconfig() { for file in /etc/sysconfig/network/ifcfg-eth*; do [ -f "$file" ] || continue # 可能两边有引号 mac=$(grep ^LLADDR= "$file" | cut -d= -f2 | sed "s/'//g" | grep .) || continue ethx=$(get_ethx_by_mac "$mac") || continue old_ethx=${file##*-} if ! [ "$old_ethx" = "$ethx" ]; then # 不要直接更改文件名,因为可能覆盖已有文件 for type in ifcfg ifroute; do old_file=/etc/sysconfig/network/$type-$old_ethx new_file=/etc/sysconfig/network/$type-$ethx.tmp # 防止没有 ifroute-eth* 导致中断脚本 if [ -f "$old_file" ]; then mv "$old_file" "$new_file" fi done fi done # 上面的循环结束后,再将 tmp 改成正式文件 for tmp_file in \ /etc/sysconfig/network/ifcfg-e*.tmp \ /etc/sysconfig/network/ifroute-e*.tmp; do if [ -f "$tmp_file" ]; then mv "$tmp_file" "${tmp_file%.tmp}" fi done } fix_network_manager() { for file in /etc/NetworkManager/system-connections/cloud-init-eth*.nmconnection; do [ -f "$file" ] || continue mac=$(grep ^mac-address= "$file" | cut -d= -f2 | grep .) || continue ethx=$(get_ethx_by_mac "$mac") || continue proper_file=/etc/NetworkManager/system-connections/$ethx.nmconnection # 更改文件内容 sed -i "s/^id=.*/id=$ethx/" "$file" # 更改文件名 mv "$file" "$proper_file" # NM 不会自动忽略 Azure 的 slave 网卡,需手动设置 # azure 文档中的方法不够通用,只适合 azure # https://learn.microsoft.com/zh-cn/azure/virtual-network/accelerated-networking-overview # 我们采用红帽的方法 # https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/8/html/configuring_and_managing_networking/configuring-networkmanager-to-ignore-certain-devices_configuring-and-managing-networking if slave_ethx=$(get_ethx_by_mac "$mac" slave); then cat >"/etc/NetworkManager/conf.d/99-$slave_ethx-unmanaged.conf" <>"$tmp_file" fi done <"$file" mv "$tmp_file" "$file" fi } fix_netplan() { file=/etc/netplan/50-cloud-init.yaml tmp_file=$file.tmp rm -f "$tmp_file" if [ -f "$file" ]; then while IFS= read -r line; do if echo "$line" | grep -Eq '^[[:space:]]+macaddress:'; then # 得到正确的网卡名 mac=$(echo "$line" | awk '{print $NF}' | sed 's/"//g') ethx=$(get_ethx_by_mac "$mac") || true elif echo "$line" | grep -Eq '^[[:space:]]+eth[0-9]+:'; then # 改成正确的网卡名 if [ -n "$ethx" ]; then line=$(echo "$line" | sed -E "s/[^[:space:]]+/$ethx:/") fi fi echo "$line" >>"$tmp_file" # 删除 set-name 不过这一步在 trans 已完成 # 因为 netplan-generator 会在 systemd generator 阶段就根据 netplan 配置重命名网卡 # systemd generator 阶段比本脚本和 systemd-networkd 更早运行 # 倒序 done < <(grep -Ev "^[[:space:]]+set-name:" "$file" | tac) # 再倒序回来 tac "$tmp_file" >"$file" rm -f "$tmp_file" # 通过 systemd netplan generator 生成 /run/systemd/network/10-netplan-enp3s0.network systemctl daemon-reload fi } fix_systemd_networkd() { for file in /etc/systemd/network/10-cloud-init-eth*.network; do [ -f "$file" ] || continue mac=$(grep ^MACAddress= "$file" | cut -d= -f2 | grep .) || continue ethx=$(get_ethx_by_mac "$mac") || continue proper_file=/etc/systemd/network/10-$ethx.network # 更改文件内容 sed -Ei "s/^Name=eth[0-9]+/Name=$ethx/" "$file" # 更改文件名 mv "$file" "$proper_file" done } fix_rh_sysconfig fix_suse_sysconfig fix_network_manager fix_ifupdown fix_netplan fix_systemd_networkd ================================================ FILE: frpc-example.toml ================================================ serverAddr = "11.22.33.44" serverPort = 7000 auth.token = "123456" [[proxies]] name = "ssh" type = "tcp" localIP = "127.0.0.1" localPort = 22 remotePort = 2222 [[proxies]] name = "rdp_tcp" type = "tcp" localIP = "127.0.0.1" localPort = 3389 remotePort = 33890 [[proxies]] name = "rdp_udp" type = "udp" localIP = "127.0.0.1" localPort = 3389 remotePort = 33890 ================================================ FILE: frpc.service ================================================ # https://github.com/archlinuxcn/repo/blob/master/archlinuxcn/frp/frpc.service [Unit] Description=Frp Client Service After=network-online.target Wants=network-online.target [Service] Type=simple User=nobody Restart=on-failure RestartSec=5s ExecStart=/usr/local/bin/frpc -c /usr/local/etc/frpc/frpc.conf ExecReload=/usr/local/bin/frpc reload -c /usr/local/etc/frpc/frpc.conf [Install] WantedBy=multi-user.target ================================================ FILE: get-frpc-url.sh ================================================ #!/bin/ash # shellcheck shell=dash # trans.sh/debian.cfg 共用此脚本 # debian 9 不支持 set -E set -e is_in_china() { grep -q 1 /dev/netconf/*/is_in_china } is_ipv6_only() { ! grep -q 1 /dev/netconf/*/ipv4_has_internet } get_frpc_url() { # 传入 windows 或者 linux local os_type=$1 local nt_ver=$2 local os_bit=${3:-64} get_old_version() { # 脚本不支持安装 32 位 linux 系统,因此不用管 if [ "$os_type" = windows ]; then # 最早支持 toml 的版本是 0.52.0 # 最后支持 vista 的版本是 0.29.0 # 最后支持 32 位的版本是 0.51.3 # 最后支持 win7 的版本是 0.54.0 case "$os_bit" in 32) case "$nt_ver" in 6.0) echo 0.29.0 ;; # vista *) echo 0.51.3 ;; # win7+ esac ;; 64) case "$nt_ver" in 6.0) echo 0.29.0 ;; # vista 6.1) echo 0.54.0 ;; # win7 # 目前最新版本 v0.66.0 依然可以在 win8 上运行 esac ;; esac fi } is_need_old_version() { [ -n "$(get_old_version)" ] } version=$( if is_need_old_version; then get_old_version else # debian 11 initrd 没有 xargs awk # debian 12 initrd 没有 xargs # github 不支持 ipv6 if is_in_china || is_ipv6_only; then wget -O- https://mirrors.nju.edu.cn/github-release/fatedier/frp/LatestRelease/frp_sha256_checksums.txt | grep -m1 frp_ | cut -d_ -f2 else # https://api.github.com/repos/fatedier/frp/releases/latest 有请求次数限制 # root@localhost:~# wget --spider -S https://github.com/fatedier/frp/releases/latest 2>&1 | grep Location: # Location: https://github.com/fatedier/frp/releases/tag/v0.62.0 # Location: https://github.com/fatedier/frp/releases/tag/v0.62.0 [following] # 原版 wget 多了这行 wget --spider -S https://github.com/fatedier/frp/releases/latest 2>&1 | grep -m1 '^ Location:' | sed 's,.*/tag/v,,' fi fi ) if [ -z "$version" ]; then echo 'cannot find version' >&2 return 1 fi suffix=$( case "$os_type" in linux) echo tar.gz ;; windows) echo zip ;; esac ) mirror=$( # nju 没有 win7 用的旧版 # github 不支持 ipv6 # daocloud 加速不支持 ipv6 # jsdelivr 不支持 github releases 文件 if is_ipv6_only; then if is_need_old_version; then echo 'NOT_SUPPORT' >&2 return 1 else echo https://mirrors.nju.edu.cn/github-release/fatedier/frp fi else if is_in_china; then if is_need_old_version; then echo https://files.m.daocloud.io/github.com/fatedier/frp/releases/download else echo https://mirrors.nju.edu.cn/github-release/fatedier/frp fi else echo https://github.com/fatedier/frp/releases/download fi fi ) arch=$( case "$(uname -m)" in x86_64) case "$os_bit" in 32) echo 386 ;; 64) echo amd64 ;; esac ;; aarch64) echo arm64 ;; esac ) filename=frp_${version}_${os_type}_${arch}.$suffix echo "${mirror}/v${version}/${filename}" } get_frpc_url "$@" ================================================ FILE: get-xda.sh ================================================ #!/bin/sh # debian ubuntu redhat 安装模式共用此脚本 # alpine 未用到此脚本 get_all_disks() { # shellcheck disable=SC2010 ls /sys/block/ | grep -Ev '^(loop|sr|nbd)' } get_xda() { # 如果没找到 main_disk 或 xda # 返回假的值,防止意外地格式化全部盘 eval "$(grep -o 'extra_main_disk=[^ ]*' /proc/cmdline | sed 's/^extra_//')" if [ -z "$main_disk" ]; then echo 'MAIN_DISK_NOT_FOUND' return 1 fi for disk in $(get_all_disks); do if fdisk -l "/dev/$disk" | grep -iq "$main_disk"; then echo "$disk" return fi done echo 'XDA_NOT_FOUND' return 1 } get_xda ================================================ FILE: initrd-network.sh ================================================ #!/bin/ash # shellcheck shell=dash # alpine/debian initrd 共用此脚本 # accept_ra 接收 RA + 自动配置网关 # autoconf 自动配置地址,依赖 accept_ra mac_addr=$1 ipv4_addr=$2 ipv4_gateway=$3 ipv6_addr=$4 ipv6_gateway=$5 is_in_china=$6 ipv6_extra_addrs=$7 DHCP_TIMEOUT=15 DNS_FILE_TIMEOUT=5 TEST_TIMEOUT=10 # 检测是否有网络是通过检测这些 IP 的端口是否开放 # 因为 debian initrd 没有 nslookup # 改成 generate_204?但检测网络时可能 resolv.conf 为空 # HTTP 80 # HTTPS/DOH 443 # DOT 853 if $is_in_china; then ipv4_dns1='223.5.5.5' ipv4_dns2='119.29.29.29' # 不开放 853 ipv6_dns1='2400:3200::1' ipv6_dns2='2402:4e00::' # 不开放 853 else ipv4_dns1='1.1.1.1' ipv4_dns2='8.8.8.8' # 不开放 80 ipv6_dns1='2606:4700:4700::1111' ipv6_dns2='2001:4860:4860::8888' # 不开放 80 fi # 找到主网卡 # debian 11 initrd 没有 xargs awk # debian 12 initrd 没有 xargs get_ethx() { # 过滤 azure vf (带 master ethx) # 2: eth0: mtu 1500 qdisc mq state UP qlen 1000\ link/ether 60:45:bd:21:8a:51 brd ff:ff:ff:ff:ff:ff # 3: eth1: mtu 1500 qdisc mq master eth0 state UP qlen 1000\ link/ether 60:45:bd:21:8a:51 brd ff:ff:ff if false; then ip -o link | grep -i "$mac_addr" | grep -v master | awk '{print $2}' | cut -d: -f1 | grep . else ip -o link | grep -i "$mac_addr" | grep -v master | cut -d' ' -f2 | cut -d: -f1 | grep . fi } get_ipv4_gateway() { # debian 11 initrd 没有 xargs awk # debian 12 initrd 没有 xargs ip -4 route show default dev "$ethx" | head -1 | cut -d ' ' -f3 } get_ipv6_gateway() { # debian 11 initrd 没有 xargs awk # debian 12 initrd 没有 xargs ip -6 route show default dev "$ethx" | head -1 | cut -d ' ' -f3 } get_first_ipv4_addr() { # debian 11 initrd 没有 xargs awk # debian 12 initrd 没有 xargs if false; then ip -4 -o addr show scope global dev "$ethx" | head -1 | awk '{print $4}' else ip -4 -o addr show scope global dev "$ethx" | head -1 | grep -o '[0-9\.]*/[0-9]*' fi } get_first_ipv4_gateway() { # debian 11 initrd 没有 xargs awk # debian 12 initrd 没有 xargs if false; then ip -4 route show default dev "$ethx" | head -1 | awk '{print $3}' else ip -4 route show default dev "$ethx" | head -1 | cut -d' ' -f3 fi } remove_netmask() { cut -d/ -f1 } get_first_ipv6_addr() { # debian 11 initrd 没有 xargs awk # debian 12 initrd 没有 xargs if false; then ip -6 -o addr show scope global dev "$ethx" | head -1 | awk '{print $4}' else ip -6 -o addr show scope global dev "$ethx" | head -1 | grep -o '[0-9a-f\:]*/[0-9]*' fi } get_first_ipv6_gateway() { # debian 11 initrd 没有 xargs awk # debian 12 initrd 没有 xargs if false; then ip -6 route show default dev "$ethx" | head -1 | awk '{print $3}' else ip -6 route show default dev "$ethx" | head -1 | cut -d' ' -f3 fi } is_have_ipv4_addr() { ip -4 addr show scope global dev "$ethx" | grep -q inet } is_have_ipv6_addr() { ip -6 addr show scope global dev "$ethx" | grep -q inet6 } is_have_ipv4_gateway() { ip -4 route show default dev "$ethx" | grep -q . } is_have_ipv6_gateway() { ip -6 route show default dev "$ethx" | grep -q . } is_have_ipv4() { is_have_ipv4_addr && is_have_ipv4_gateway } is_have_ipv6() { is_have_ipv6_addr && is_have_ipv6_gateway } is_have_ipv4_dns() { [ -f /etc/resolv.conf ] && grep -q '^nameserver .*\.' /etc/resolv.conf } is_have_ipv6_dns() { [ -f /etc/resolv.conf ] && grep -q '^nameserver .*:' /etc/resolv.conf } add_missing_ipv4_config() { if [ -n "$ipv4_addr" ] && [ -n "$ipv4_gateway" ]; then if ! is_have_ipv4_addr; then ip -4 addr add "$ipv4_addr" dev "$ethx" fi if ! is_have_ipv4_gateway; then # 如果 dhcp 无法设置onlink网关,那么在这里设置 # debian 9 ipv6 不能识别 onlink,但 ipv4 能识别 onlink if true; then ip -4 route add "$ipv4_gateway" dev "$ethx" ip -4 route add default via "$ipv4_gateway" dev "$ethx" else ip -4 route add default via "$ipv4_gateway" dev "$ethx" onlink fi fi fi } add_missing_ipv6_config() { if [ -n "$ipv6_addr" ] && [ -n "$ipv6_gateway" ]; then if ! is_have_ipv6_addr; then ip -6 addr add "$ipv6_addr" dev "$ethx" fi if ! is_have_ipv6_gateway; then # 如果 dhcp 无法设置onlink网关,那么在这里设置 # debian 9 ipv6 不能识别 onlink if true; then ip -6 route add "$ipv6_gateway" dev "$ethx" ip -6 route add default via "$ipv6_gateway" dev "$ethx" else ip -6 route add default via "$ipv6_gateway" dev "$ethx" onlink fi fi # 添加额外的 IPv6 地址(逗号分隔) if [ -n "$ipv6_extra_addrs" ]; then printf '%s\n' "$ipv6_extra_addrs" | tr ',' '\n' | while IFS= read -r addr; do if [ -n "$addr" ]; then ip -6 addr add "$addr" dev "$ethx" 2>/dev/null || true fi done fi fi } is_need_test_ipv4() { is_have_ipv4 && ! $ipv4_has_internet } is_need_test_ipv6() { is_have_ipv6 && ! $ipv6_has_internet } # 测试方法: # ping 有的机器禁止 # nc 测试 dot doh 端口是否开启 # wget 测试下载 # initrd 里面的软件版本,是否支持指定源IP/网卡 # 软件 nc wget nslookup # debian9 × √ 没有此软件 # alpine √ × × test_by_wget() { src=$1 dst=$2 # ipv6 需要添加 [] if echo "$dst" | grep -q ':'; then url="https://[$dst]" else url="https://$dst" fi # tcp 443 通了就算成功,不管 http 是不是 404 # grep -m1 快速返回 wget -T "$TEST_TIMEOUT" \ --bind-address="$src" \ --no-check-certificate \ --max-redirect 0 \ --tries 1 \ -O /dev/null \ "$url" 2>&1 | grep -iq -m1 connected } test_by_nc() { src=$1 dst=$2 # tcp 443 通了就算成功 nc -z -v \ -w "$TEST_TIMEOUT" \ -s "$src" \ "$dst" 443 } is_debian_kali() { [ -f /etc/lsb-release ] && grep -Eiq 'Debian|Kali' /etc/lsb-release } test_connect() { if is_debian_kali; then test_by_wget "$1" "$2" else test_by_nc "$1" "$2" fi } test_internet() { for i in $(seq 5); do echo "Testing Internet Connection. Test $i... " if is_need_test_ipv4 && current_ipv4_addr="$(get_first_ipv4_addr | remove_netmask)" && { test_connect "$current_ipv4_addr" "$ipv4_dns1" || test_connect "$current_ipv4_addr" "$ipv4_dns2"; } >/dev/null 2>&1; then echo "IPv4 has internet." ipv4_has_internet=true fi if is_need_test_ipv6 && current_ipv6_addr="$(get_first_ipv6_addr | remove_netmask)" && { test_connect "$current_ipv6_addr" "$ipv6_dns1" || test_connect "$current_ipv6_addr" "$ipv6_dns2"; } >/dev/null 2>&1; then echo "IPv6 has internet." ipv6_has_internet=true fi if ! is_need_test_ipv4 && ! is_need_test_ipv6; then break fi sleep 1 done } flush_ipv4_config() { ip -4 addr flush scope global dev "$ethx" ip -4 route flush dev "$ethx" # DHCP 获取的 IP 不是重装前的 IP 时,一并删除 DHCP 获取的 DNS,以防 DNS 无效 sed -i "/\./d" /etc/resolv.conf } should_disable_dhcpv4=false should_disable_accept_ra=false should_disable_autoconf=false flush_ipv6_config() { if $should_disable_accept_ra; then echo 0 >"/proc/sys/net/ipv6/conf/$ethx/accept_ra" fi if $should_disable_autoconf; then echo 0 >"/proc/sys/net/ipv6/conf/$ethx/autoconf" fi ip -6 addr flush scope global dev "$ethx" ip -6 route flush dev "$ethx" # DHCP 获取的 IP 不是重装前的 IP 时,一并删除 DHCP 获取的 DNS,以防 DNS 无效 sed -i "/:/d" /etc/resolv.conf } for i in $(seq 20); do if ethx=$(get_ethx); then break fi sleep 1 done if [ -z "$ethx" ]; then echo "Not found network card: $mac_addr" exit fi echo "Configuring $ethx ($mac_addr)..." # 不开启 lo 则 frp 无法连接 127.0.0.1 22 ip link set dev lo up # 开启 ethx ip link set dev "$ethx" up sleep 1 # 开启 dhcpv4/v6 # debian / kali if [ -f /usr/share/debconf/confmodule ]; then # shellcheck source=/dev/null . /usr/share/debconf/confmodule db_progress STEP 1 # dhcpv4 # 无需等待写入 dns,在 dhcpv6 等待 db_progress INFO netcfg/dhcp_progress udhcpc -i "$ethx" -f -q -n || true db_progress STEP 1 # slaac + dhcpv6 db_progress INFO netcfg/slaac_wait_title # https://salsa.debian.org/installer-team/netcfg/-/blob/master/autoconfig.c#L148 cat </var/lib/netcfg/dhcp6c.conf interface $ethx { send ia-na 0; request domain-name-servers; request domain-name; script "/lib/netcfg/print-dhcp6c-info"; }; id-assoc na 0 { }; EOF dhcp6c -c /var/lib/netcfg/dhcp6c.conf "$ethx" || true sleep $DHCP_TIMEOUT # 等待获取 ip 和写入 dns # kill-all-dhcp kill -9 "$(cat /var/run/dhcp6c.pid)" || true db_progress STEP 1 # 静态 + 检测网络提示 db_subst netcfg/link_detect_progress interface "$ethx" db_progress INFO netcfg/link_detect_progress else # alpine # h3c 移动云电脑使用 udhcpc 会重复提示 sending select,因此添加 timeout 强制结束进程 # dhcpcd 会配置租约时间,过期会移除 IP,但我们的没有在后台运行 dhcpcd ,因此用 udhcpc method=udhcpc case "$method" in udhcpc) timeout $DHCP_TIMEOUT udhcpc -i "$ethx" -f -q -n || true timeout $DHCP_TIMEOUT udhcpc6 -i "$ethx" -f -q -n || true sleep $DNS_FILE_TIMEOUT # 好像不用等待写入 dns,但是以防万一 ;; dhcpcd) # https://gitlab.alpinelinux.org/alpine/aports/-/blob/master/main/dhcpcd/dhcpcd.pre-install grep -q dhcpcd /etc/group || addgroup -S dhcpcd grep -q dhcpcd /etc/passwd || adduser -S -D -H \ -h /var/lib/dhcpcd \ -s /sbin/nologin \ -G dhcpcd \ -g dhcpcd \ dhcpcd # --noipv4ll 禁止生成 169.254.x.x if false; then # 等待 DHCP 全过程 timeout $DHCP_TIMEOUT \ dhcpcd --persistent --noipv4ll --nobackground "$ethx" else # 等待 DNS dhcpcd --persistent --noipv4ll "$ethx" # 获取到 IP 后立即切换到后台 sleep $DNS_FILE_TIMEOUT # 需要等待写入 dns dhcpcd -x "$ethx" # 终止 fi # autoconf 和 accept_ra 会被 dhcpcd 自动关闭,因此需要重新打开 # 如果没重新打开,重新运行 dhcpcd 命令依然可以正常生成 slaac 地址和路由 sysctl -w "net.ipv6.conf.$ethx.autoconf=1" sysctl -w "net.ipv6.conf.$ethx.accept_ra=1" ;; esac fi # 等待slaac # 有ipv6地址就跳过,不管是slaac或者dhcpv6 # 因为会在trans里判断 # 这里等待5秒就够了,因为之前尝试获取dhcp6也用了一段时间 for i in $(seq 5 -1 0); do is_have_ipv6 && break echo "waiting slaac for ${i}s" sleep 1 done # 记录是否有动态地址 # 由于还没设置静态ip,所以有条目表示有动态地址 is_have_ipv4_addr && dhcpv4=true || dhcpv4=false is_have_ipv6_addr && dhcpv6_or_slaac=true || dhcpv6_or_slaac=false is_have_ipv6_gateway && ra_has_gateway=true || ra_has_gateway=false # 如果自动获取的 IP 不是重装前的,则改成静态,使用之前的 IP # 只比较 IP,不比较掩码/网关,因为 # 1. 假设掩码/网关导致无法上网,后面也会检测到并改成静态 # 2. openSUSE wicked dhcpv6 是 64 位掩码,aws lightsail 模板上的也是,而其它 dhcpv6 软件都是 128 位掩码 if $dhcpv4 && [ -n "$ipv4_addr" ] && [ -n "$ipv4_gateway" ] && ! [ "$(echo "$ipv4_addr" | cut -d/ -f1)" = "$(get_first_ipv4_addr | cut -d/ -f1)" ]; then echo "IPv4 address obtained from DHCP is different from old system." should_disable_dhcpv4=true flush_ipv4_config fi if $dhcpv6_or_slaac && [ -n "$ipv6_addr" ] && [ -n "$ipv6_gateway" ] && ! [ "$(echo "$ipv6_addr" | cut -d/ -f1)" = "$(get_first_ipv6_addr | cut -d/ -f1)" ]; then echo "IPv6 address obtained from SLAAC/DHCPv6 is different from old system." should_disable_accept_ra=true should_disable_autoconf=true flush_ipv6_config fi # 设置静态地址,或者设置 debian 9 udhcpc 无法设置的网关 add_missing_ipv4_config add_missing_ipv6_config # 检查 ipv4/ipv6 是否连接联网 ipv4_has_internet=false ipv6_has_internet=false test_internet # 如果无法上网,并且自动获取的 掩码/网关 不是重装前的,则改成静态 # ip_addr 包括 IP/掩码,所以可以用来判断掩码是否不同 # IP 不同的情况在前面已经改成静态了 if ! $ipv4_has_internet && $dhcpv4 && [ -n "$ipv4_addr" ] && [ -n "$ipv4_gateway" ] && ! { [ "$ipv4_addr" = "$(get_first_ipv4_addr)" ] && [ "$ipv4_gateway" = "$(get_first_ipv4_gateway)" ]; }; then echo "IPv4 netmask/gateway obtained from DHCP is different from old system." should_disable_dhcpv4=true flush_ipv4_config add_missing_ipv4_config test_internet fi # 有可能是静态 IPv6 但能从 RA 获取到网关,因此加上 || $ra_has_gateway if ! $ipv6_has_internet && { $dhcpv6_or_slaac || $ra_has_gateway; } && [ -n "$ipv6_addr" ] && [ -n "$ipv6_gateway" ] && ! { [ "$ipv6_addr" = "$(get_first_ipv6_addr)" ] && [ "$ipv6_gateway" = "$(get_first_ipv6_gateway)" ]; }; then echo "IPv6 netmask/gateway obtained from SLAAC/DHCPv6 is different from old system." should_disable_accept_ra=true should_disable_autoconf=true flush_ipv6_config add_missing_ipv6_config test_internet fi # 要删除不联网协议的ip,因为 # 1 甲骨文云管理面板添加ipv6地址然后取消 # 依然会分配ipv6地址,但ipv6没网络 # 此时alpine只会用ipv6下载apk,而不用会ipv4下载 # 2 有ipv4地址但没有ipv4网关的情况(vultr $2.5 ipv6 only),aria2会用ipv4下载 # 假设 ipv4 ipv6 在不同网卡,ipv4 能上网但 ipv6 不能上网,这时也要删除 ipv6 # 不能用 ipv4_has_internet && ! ipv6_has_internet 判断,因为它判断的是同一个网卡 if ! $ipv4_has_internet; then if $dhcpv4; then should_disable_dhcpv4=true fi flush_ipv4_config fi if ! $ipv6_has_internet; then # 防止删除 IPv6 后再次通过 SLAAC 获得 # 不用判断 || $ra_has_gateway ,因为没有 IPv6 地址但有 IPv6 网关时,不会出现下载问题 if $dhcpv6_or_slaac; then should_disable_accept_ra=true should_disable_autoconf=true fi flush_ipv6_config fi # 如果联网了,但没获取到默认 DNS,则添加我们的 DNS # 有一种情况是,多网卡,且能上网的网卡先完成了这个脚本,不能上网的网卡后完成 # 无法上网的网卡通过 flush_ipv4_config 删除了不能上网的 IP 和 dns # (原计划是删除无法上网的网卡 dhcp4 获取的 dns,但实际上无法区分) # 因此这里直接添加 dns,不判断是否联网 if ! is_have_ipv4_dns; then echo "nameserver $ipv4_dns1" >>/etc/resolv.conf echo "nameserver $ipv4_dns2" >>/etc/resolv.conf fi if ! is_have_ipv6_dns; then echo "nameserver $ipv6_dns1" >>/etc/resolv.conf echo "nameserver $ipv6_dns2" >>/etc/resolv.conf fi # 传参给 trans.start netconf="/dev/netconf/$ethx" mkdir -p "$netconf" $dhcpv4 && echo 1 >"$netconf/dhcpv4" || echo 0 >"$netconf/dhcpv4" $dhcpv6_or_slaac && echo 1 >"$netconf/dhcpv6_or_slaac" || echo 0 >"$netconf/dhcpv6_or_slaac" $should_disable_dhcpv4 && echo 1 >"$netconf/should_disable_dhcpv4" || echo 0 >"$netconf/should_disable_dhcpv4" $should_disable_accept_ra && echo 1 >"$netconf/should_disable_accept_ra" || echo 0 >"$netconf/should_disable_accept_ra" $should_disable_autoconf && echo 1 >"$netconf/should_disable_autoconf" || echo 0 >"$netconf/should_disable_autoconf" $is_in_china && echo 1 >"$netconf/is_in_china" || echo 0 >"$netconf/is_in_china" echo "$ethx" >"$netconf/ethx" echo "$mac_addr" >"$netconf/mac_addr" echo "$ipv4_addr" >"$netconf/ipv4_addr" echo "$ipv4_gateway" >"$netconf/ipv4_gateway" echo "$ipv6_addr" >"$netconf/ipv6_addr" echo "$ipv6_gateway" >"$netconf/ipv6_gateway" echo "$ipv6_extra_addrs" >"$netconf/ipv6_extra_addrs" $ipv4_has_internet && echo 1 >"$netconf/ipv4_has_internet" || echo 0 >"$netconf/ipv4_has_internet" $ipv6_has_internet && echo 1 >"$netconf/ipv6_has_internet" || echo 0 >"$netconf/ipv6_has_internet" ================================================ FILE: logviewer-nginx.conf ================================================ server { listen @WEB_PORT@; listen [::]:@WEB_PORT@; root /; gzip on; gzip_types text/plain; location = / { try_files /logviewer.html 404; } location = /reinstall.log { types { text/plain log; } try_files $uri 404; } location / { return 404; } } ================================================ FILE: logviewer.html ================================================ Reinstall Logs

    

    

    





================================================
FILE: redhat.cfg
================================================
# shellcheck disable=SC2148
# 设置
keyboard --vckeymap=us --xlayouts='us'
lang en_US.UTF-8
timezone Asia/Shanghai --utc
rootpw --plaintext 123@@@
text
reboot
%include /tmp/include-url-command

# 分区
%include /tmp/include-disk-only-use
%include /tmp/include-bootloader
clearpart --all --initlabel
reqpart # 如果需要,自动创建 efi 或 biosboot 分区
part / --fstype=xfs --grow

# 软件
%packages --ignoremissing # el9 minimal.iso fedora Server repo/iso 没有 tuned
@^Minimal Install
%include /tmp/include-packages-for-resize
%include /tmp/exclude-packages-for-vm
%end

# 禁用防火墙
# firewall --disabled

# 禁用 selinux
selinux --disabled

# 禁用 kdump
%addon com_redhat_kdump --disable
%end

##############################################
%pre
distro=$(awk -F: '{ print $3 }' $include

# 设置 tty
include=/tmp/include-bootloader
# shellcheck disable=SC2154
console_cmdline=$(wget --no-check-certificate --tries=5 "$confhome/ttys.sh" -O- | sh -s console=)
echo "bootloader --append=\"$console_cmdline\"" >$include

# 有 installer 分区,表示用了两步安装
include=/tmp/include-packages-for-resize
touch $include
if [ -e /dev/disk/by-label/installer ]; then
    # 1g内存下,安装器默认开启了zram ,但安装f38还是不够内存
    # 具体表现为不断重启安装界面,所以还要开启swap
    ram_size=$(lsmem -b 2>/dev/null | grep 'Total online memory:' | awk '{ print $NF/1024/1024 }')
    if [ -z "$ram_size" ] || [ "$ram_size" -le 1024 ]; then
        mount /dev/disk/by-label/installer /run/install/repo -o remount,rw
        swapfile=/run/install/repo/swapfile
        if command -v fallocate; then
            fallocate -l 1G $swapfile
        else
            dd if=/dev/zero of=$swapfile bs=1M count=1024
        fi
        chmod 0600 $swapfile
        mkswap $swapfile
        swapon $swapfile
    fi

    # feroda 默认不包含 cronie
    echo cronie >>$include

    # el7 的parted不支持在线扩容,要用 growpart 和 gdisk 处理 gpt 分区
    if [ "$releasever" = "7" ]; then
        echo cloud-utils-growpart >>$include
        echo gdisk >>$include
    fi
fi

# 排除虚拟机用不上的组件
include=/tmp/exclude-packages-for-vm
touch $include
if systemd-detect-virt -v; then
    cat <$include
        # 不删除usb相关的包 因为甲骨文云有usb设备 作用未知
        # -usb_modeswitch
        # -usbutils

        # 无线
        -iw
        -crda
        -rfkill
        -iwl*-firmware

        # 其他
        -irqbalance # 多核+直通设备可能有用?
        -microcode_ctl
        -smartmontools

        # 各种固件
        -aic94xx-firmware
        -alsa-firmware
        -ivtv-firmware
        # -linux-firmware # 去除后安装centos 8会报错

        # fedora 特有固件
        -amd-gpu-firmware
        -atheros-firmware
        -brcmfmac-firmware
        -intel-gpu-firmware
        -mt7xxx-firmware
        -nvidia-gpu-firmware
        -realtek-firmware
EOF
fi

# 设置安装源
include=/tmp/include-url-command
# shellcheck disable=SC2154
if [ "$localtest" = 1 ]; then
    echo "url --url=$confhome/$releasever/" >$include
    # echo cdrom >$include
else
    echo "url --mirrorlist=$mirrorlist" >$include
    # 对于el7/fedora, 添加了 updates repo 才会安装最新的包
    if [ "$releasever" = "7" ] || [ "$distro" = "fedoraproject" ]; then
        echo "repo --name=updates" >>$include
    fi
fi
%end

##############################################
%post
# el9/fedora的sshd默认不允许root密码登录,需手动开启
# rootpw --allow-ssh 9.1 以上才支持
distro=$(awk -F: '{ print $3 }' /etc/ssh/sshd_config.d/01-permitrootlogin.conf
fi

# 分步安装的系统,要将最后一个分区(installer)合并到系统分区
if [ -e /dev/disk/by-label/installer ]; then
    # 提取 extra_localtest extra_confhome extra_mirrorlist
    prefix=extra
    for var in $(grep -o "\b${prefix}_[^ ]*" /proc/cmdline | xargs); do
        eval "$(echo "$var" | sed -E "s/${prefix}_([^=]*)=(.*)/\1='\2'/")"
    done

    cd /
    curl -O "$confhome/resize.sh"
    echo '@reboot root bash /resize.sh' >/etc/cron.d/resize
fi
%end


================================================
FILE: reinstall.bat
================================================
@echo off
mode con cp select=437 >nul
setlocal EnableDelayedExpansion

set confhome=https://raw.githubusercontent.com/bin456789/reinstall/main
set confhome_cn=https://cnb.cool/bin456789/reinstall/-/git/raw/main
rem set confhome_cn=https://www.ghproxy.cc/https://raw.githubusercontent.com/bin456789/reinstall/main

set pkgs=curl,cpio,p7zip,dos2unix,jq,xz,gzip,zstd,openssl,bind-utils,libiconv,binutils
set cmds=curl,cpio,p7zip,dos2unix,jq,xz,gzip,zstd,openssl,nslookup,iconv,ar

rem 65001 代码页会乱码

rem 不要用 :: 注释
rem 否则可能会出现 系统找不到指定的驱动器

rem Windows 7 SP1 winhttp 默认不支持 tls 1.2
rem https://support.microsoft.com/en-us/topic/update-to-enable-tls-1-1-and-tls-1-2-as-default-secure-protocols-in-winhttp-in-windows-c4bd73d2-31d7-761e-0178-11268bb10392
rem 有些系统根证书没更新
rem 所以不要用https
rem 进入脚本目录
cd /d %~dp0

rem 检查是否有管理员权限
fltmc >nul 2>&1
if errorlevel 1 (
    echo Please run as administrator^^!
    exit /b
)

rem 有时 %tmp% 带会话 id,且文件夹不存在
rem https://learn.microsoft.com/troubleshoot/windows-server/shell-experience/temp-folder-with-logon-session-id-deleted
rem if not exist %tmp% (
rem     md %tmp%
rem )

rem 下载 geoip
if not exist geoip (
    rem www.cloudflare.com/dash.cloudflare.com 国内访问的是美国服务器,而且部分地区被墙
    call :download http://www.qualcomm.cn/cdn-cgi/trace %~dp0geoip || goto :download_failed
)

rem 判断是否有 loc=
findstr /c:"loc=" geoip >nul
if errorlevel 1 (
    echo Invalid geoip file
    del geoip
    exit /b 1
)

rem 检查是否国内
findstr /c:"loc=CN" geoip >nul
if not errorlevel 1 (
    rem mirrors.tuna.tsinghua.edu.cn 会强制跳转 https
    set mirror=http://mirror.nju.edu.cn
    if defined confhome_cn (
        set confhome=!confhome_cn!
    ) else if defined github_proxy (
        echo !confhome! | findstr /c:"://raw.githubusercontent.com/" >nul
        if not errorlevel 1 (
            set confhome=!confhome:http://=https://!
            set confhome=!confhome:https://raw.githubusercontent.com=%github_proxy%!
        )
    )
) else (
    rem 服务器在美国 equinix 机房,不是 cdn
    set mirror=http://mirrors.kernel.org
)

call :check_cygwin_installed || (
    rem win10 arm 支持运行 x86 软件
    rem win11 arm 支持运行 x86 和 x86_64 软件

    rem windows 11 24h2 没有 wmic
    rem wmic os get osarchitecture 显示中文,即使设置了 mode con cp select=437
    rem wmic ComputerSystem get SystemType 显示英文
    rem for /f "tokens=*" %%a in ('wmic ComputerSystem get SystemType ^| find /i "based"') do (
    rem     set "SystemType=%%a"
    rem )

    rem 有的系统精简了 powershell
    rem for /f "delims=" %%a in ('powershell -NoLogo -NoProfile -NonInteractive -Command "(Get-WmiObject win32_computersystem).SystemType"') do (
    rem     set "SystemType=%%a"
    rem )

    rem SystemArch
    for /f "tokens=3" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v PROCESSOR_ARCHITECTURE') do (
        set SystemArch=%%a
    )

    rem 也可以用 PROCESSOR_ARCHITEW6432 和 PROCESSOR_ARCHITECTURE 判断
    rem ARM64 win11  PROCESSOR_ARCHITEW6432   PROCESSOR_ARCHITECTURE
    rem 原生cmd          未定义                      ARM64
    rem 32位cmd          ARM64                       x86

    rem if defined PROCESSOR_ARCHITEW6432 (
    rem     set "SystemArch=%PROCESSOR_ARCHITEW6432%"
    rem ) else (
    rem     set "SystemArch=%PROCESSOR_ARCHITECTURE%"
    rem )

    rem BuildNumber
    for /f "tokens=3" %%a in ('reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion" /v CurrentBuildNumber') do (
        set /a BuildNumber=%%a
    )

    set CygwinEOL=1

    echo !SystemArch! | find "ARM" > nul
    if not errorlevel 1 (
        if !BuildNumber! GEQ 22000 (
            set CygwinEOL=0
        )
    ) else (
        echo !SystemArch! | find "AMD64" > nul
        if not errorlevel 1 (
            if !BuildNumber! GEQ 9600 (
                set CygwinEOL=0
            )
        )
    )

    rem win7/8 cygwin 已 EOL,不能用最新 cygwin 源,而要用 Cygwin Time Machine 源
    rem 但 Cygwin Time Machine 没有国内源
    rem 为了保证国内下载速度, cygwin EOL 统一使用 cygwin-archive x86 源
    if !CygwinEOL! == 1 (
        set CygwinArch=x86
        set dir=/sourceware/cygwin-archive/20221123
    ) else (
        set CygwinArch=x86_64
        set dir=/sourceware/cygwin
    )

    rem daocloud 加速有 90 天缓存,且不支持 IPv6
    rem https://github.com/DaoCloud/public-binary-files-mirror
    rem 无法用查询字符串强制刷新缓存
    rem https://files.m.daocloud.io/www.cloudflare.com/cdn-cgi/trace?a=1
    rem https://files.m.daocloud.io/www.cloudflare.com/cdn-cgi/trace?b=2
    rem 也就无法用 https://www.cygwin.com/setup-x86_64.exe?xxx=20250101 强制每天刷新缓存

    rem 下载 Cygwin
    if not exist setup-!CygwinArch!.exe (
        call :download http://www.cygwin.com/setup-!CygwinArch!.exe %~dp0setup-!CygwinArch!.exe || goto :download_failed
    )

    rem 少于 1M 视为无效
    rem 有的 IP 被官网拉黑,无法下载 exe,下载得到 html
    for %%A in (setup-!CygwinArch!.exe) do if %%~zA LSS 1048576 (
        echo Invalid Cgywin installer
        del setup-!CygwinArch!.exe
        exit /b 1
    )

    rem 安装 Cygwin
    set site=!mirror!!dir!
    start /wait setup-!CygwinArch!.exe ^
        --allow-unsupported-windows ^
        --quiet-mode ^
        --only-site ^
        --site !site! ^
        --root %SystemDrive%\cygwin ^
        --local-package-dir %~dp0cygwin-local-package-dir ^
        --packages %pkgs%

    rem 检查 Cygwin 是否成功安装
    if errorlevel 1 goto :install_cygwin_failed
    call :check_cygwin_installed || goto :install_cygwin_failed
)

rem 在c盘根目录下执行 cygpath -ua . 会得到 /cygdrive/c,因此末尾要有 /
for /f %%a in ('%SystemDrive%\cygwin\bin\cygpath -ua ./') do set thisdir=%%a

rem 下载 reinstall.sh
if not exist reinstall.sh (
    call :download_with_curl %confhome%/reinstall.sh %thisdir%reinstall.sh || goto :download_failed
    call :chmod a+x %thisdir%reinstall.sh
)

rem %* 无法处理 --iso https://x.com/?yyy=123
rem 为每个参数添加引号,使参数正确传递到 bash
rem for %%a in (%*) do (
rem     set "param=!param! "%%~a""
rem )

rem 转成 unix 格式,避免用户用 windows 记事本编辑后换行符不对
%SystemDrive%\cygwin\bin\dos2unix -q '%thisdir%reinstall.sh'

rem 用 bash 运行
rem %SystemDrive%\cygwin\bin\bash -l %thisdir%reinstall.sh %* 运行后会清屏
rem 因此不能用 -l
rem 这就需要在 reinstall.sh 里运行 source /etc/profile
rem 或者添加 export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH
%SystemDrive%\cygwin\bin\bash %thisdir%reinstall.sh %*
exit /b

rem bits 要求有 Content-Length 才能下载
rem cloudflare 的 cdn-cgi/trace 没有 Content-Length
rem 据说如果网络设为“按流量计费” bits 也无法下载
rem https://learn.microsoft.com/en-us/windows/win32/bits/http-requirements-for-bits-downloads
rem bitsadmin /transfer "%~3" /priority foreground %~1 %~2

:download
rem certutil 会被 windows Defender 报毒
rem windows server 2019 要用第二条 certutil 命令
echo Downloading: %~1 %~2
del /q "%~2" 2>nul
if exist "%~2" (echo Cannot delete %~2 & exit /b 1)

certutil -urlcache -f -split "%~1" "%~2" >nul
if not errorlevel 1 if exist "%~2" exit /b 0

certutil -urlcache -split "%~1" "%~2" >nul
if not errorlevel 1 if exist "%~2" exit /b 0

rem 下载失败时删除文件,防止下载了一部分导致下次运行时跳过了下载
del /q "%~2" 2>nul
exit /b 1

:download_with_curl
rem 加 --insecure 防止以下错误
rem curl: (77) error setting certificate verify locations:
rem   CAfile: /etc/ssl/certs/ca-certificates.crt
rem   CApath: none
echo Download: %~1 %~2
%SystemDrive%\cygwin\bin\curl -L --insecure "%~1" -o "%~2"
exit /b

:chmod
%SystemDrive%\cygwin\bin\chmod "%~1" "%~2"
exit /b

:download_failed
echo Download failed.
exit /b 1

:install_cygwin_failed
echo Failed to install Cygwin.
exit /b 1

:check_cygwin_installed
set "cmds_space=%cmds:,= %"
for %%c in (%cmds_space%) do (
    if not exist "%SystemDrive%\cygwin\bin\%%c" if not exist "%SystemDrive%\cygwin\bin\%%c.exe" (
        exit /b 1
    )
)
exit /b 0


================================================
FILE: reinstall.sh
================================================
#!/usr/bin/env sh
# shellcheck shell=bash
# shellcheck disable=SC2086

# nixos 默认的配置不会生成 /bin/bash,因此需要用 /usr/bin/env
# alpine 默认没有 bash,因此 shebang 用 sh,再 exec 切换到 bash

set -eE
confhome=https://raw.githubusercontent.com/bin456789/reinstall/main
confhome_cn=https://cnb.cool/bin456789/reinstall/-/git/raw/main
# confhome_cn=https://www.ghproxy.cc/https://raw.githubusercontent.com/bin456789/reinstall/main

# 用于判断 reinstall.sh 和 trans.sh 是否兼容
SCRIPT_VERSION=4BACD833-A585-23BA-6CBB-9AA4E08E0004

# 记录要用到的 windows 程序,运行时输出删除 \r
WINDOWS_EXES='cmd powershell wmic reg diskpart netsh bcdedit mountvol'

BOOT_ENTEY_START_MARK='### BEGIN reinstall.sh ###'
BOOT_ENTEY_END_MARK='### END reinstall.sh ###'

# 临时目录
# 不用 /tmp,因为 /tmp 挂载在内存的话,可能不够空间
tmp=/reinstall-tmp

# 强制 linux 程序输出英文,防止 grep 不到想要的内容
# https://www.gnu.org/software/gettext/manual/html_node/The-LANGUAGE-variable.html
export LC_ALL=C

# 处理部分用户用 su 切换成 root 导致环境变量没 sbin 目录
# 也能处理 cygwin bash 没有添加 -l 运行 reinstall.sh
# 不要漏了最后的 $PATH,否则会找不到 windows 系统程序例如 diskpart
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH

# 如果不是 bash 的话,继续执行会有语法错误,因此在这里判断是否 bash
if [ -z "$BASH" ]; then
    if ! command -v bash >/dev/null; then
        if [ -f /etc/alpine-release ]; then
            if ! apk add bash; then
                echo "Error while install bash." >&2
                exit 1
            fi
        else
            echo "Please run this script with bash." >&2
            exit 1
        fi
    fi
    exec bash "$0" "$@"
fi

# 好像跟 trap SIGINT 有冲突
# 记录日志,过滤含有 password 的行
# exec > >(tee >(grep -iv password >>/reinstall.log)) 2>&1
THIS_SCRIPT=$(readlink -f "$0")
trap 'trap_err $LINENO $?' ERR

trap_err() {
    line_no=$1
    ret_no=$2

    error "Line $line_no return $ret_no"
    sed -n "$line_no"p "$THIS_SCRIPT"
}

is_in_windows() {
    [ "$(uname -o)" = Cygwin ] || [ "$(uname -o)" = Msys ]
}

if is_in_windows; then
    reinstall_____='.\reinstall.bat'
else
    reinstall_____='sh reinstall.sh'
fi

usage_and_exit() {
    cat <&2
}

warn() {
    local msg
    if [ "$1" = false ]; then
        shift
        msg=$*
    else
        msg="Warning: $*"
    fi
    echo_color_text '\e[33m' "$msg" >&2
}

error() {
    echo_color_text '\e[31m' "***** ERROR *****" >&2
    echo_color_text '\e[31m' "$*" >&2
}

echo_color_text() {
    color="$1"
    shift
    plain="\e[0m"
    echo -e "$color$*$plain"
}

error_and_exit() {
    error "$@"
    exit 1
}

show_dd_password_tips() {
    warn false "
This password is only used for SSH access to view logs during the installation.
Password of the image will NOT modify.

密码仅用于安装过程中通过 SSH 查看日志。
镜像的密码不会被修改。
"
}

show_url_in_args() {
    while [ $# -gt 0 ]; do
        case "$1" in
        [Hh][Tt][Tt][Pp][Ss]://* | [Hh][Tt][Tt][Pp]://* | [Mm][Aa][Gg][Nn][Ee][Tt]:*) echo "$1" ;;
        esac
        shift
    done
}

curl() {
    is_have_cmd curl || install_pkg curl

    # 显示 url
    show_url_in_args "$@" >&2

    # 添加 -f, --fail,不然 404 退出码也为0
    # 32位 cygwin 已停止更新,证书可能有问题,先添加 --insecure
    # centos 7 curl 不支持 --retry-connrefused --retry-all-errors
    # 因此手动 retry
    for i in $(seq 5); do
        if command curl --insecure --connect-timeout 10 -f "$@"; then
            return
        else
            ret=$?
            # 403 404 错误,或者达到重试次数
            if [ $ret -eq 22 ] || [ $i -eq 5 ]; then
                return $ret
            fi
            sleep 1
        fi
    done
}

mask2cidr() {
    local x=${1##*255.}
    set -- 0^^^128^192^224^240^248^252^254^ $(((${#1} - ${#x}) * 2)) ${x%%.*}
    x=${1%%"$3"*}
    echo $(($2 + (${#x} / 4)))
}

is_in_china() {
    [ "$force_cn" = 1 ] && return 0

    if [ -z "$_loc" ]; then
        # www.cloudflare.com/dash.cloudflare.com 国内访问的是美国服务器,而且部分地区被墙
        # 没有ipv6 www.visa.cn
        # 没有ipv6 www.bose.cn
        # 没有ipv6 www.garmin.com.cn
        # 备用 www.prologis.cn
        # 备用 www.autodesk.com.cn
        # 备用 www.keysight.com.cn
        if ! _loc=$(curl -L http://www.qualcomm.cn/cdn-cgi/trace | grep '^loc=' | cut -d= -f2 | grep .); then
            error_and_exit "Can not get location."
        fi
        echo "Location: $_loc" >&2
    fi
    [ "$_loc" = CN ]
}

is_in_windows() {
    [ "$(uname -o)" = Cygwin ] || [ "$(uname -o)" = Msys ]
}

is_in_alpine() {
    [ -f /etc/alpine-release ]
}

is_use_cloud_image() {
    [ -n "$cloud_image" ] && [ "$cloud_image" = 1 ]
}

is_force_use_installer() {
    [ -n "$installer" ] && [ "$installer" = 1 ]
}

is_use_dd() {
    [ "$distro" = dd ]
}

is_boot_in_separate_partition() {
    mount | grep -q ' on /boot type '
}

is_os_in_btrfs() {
    mount | grep -q ' on / type btrfs '
}

is_os_in_subvol() {
    subvol=$(awk '($2=="/") { print $i }' /proc/mounts | grep -o 'subvol=[^ ]*' | cut -d= -f2)
    [ "$subvol" != / ]
}

get_os_part() {
    awk '($2=="/") { print $1 }' /proc/mounts
}

umount_all() {
    # windows defender 打开时,cygwin 运行 mount 很慢,但 cat /proc/mounts 很快
    if mount_lists=$(mount | grep -w "on $1" | awk '{print $3}' | grep .); then
        # alpine 没有 -R
        if umount --help 2>&1 | grep -wq -- '-R'; then
            umount -R "$1"
        else
            echo "$mount_lists" | tac | xargs -n1 umount
        fi
    fi
}

cp_to_btrfs_root() {
    mount_dir=$tmp/reinstall-btrfs-root
    if ! grep -q $mount_dir /proc/mounts; then
        mkdir -p $mount_dir
        mount "$(get_os_part)" $mount_dir -t btrfs -o subvol=/
    fi
    cp -rf "$@" "$mount_dir"
}

is_host_has_ipv4_and_ipv6() {
    host=$1

    install_pkg dig
    # dig会显示cname结果,cname结果以.结尾,grep -v '\.$' 用于去除 cname 结果
    res=$(dig +short $host A $host AAAA | grep -v '\.$')
    # 有.表示有ipv4地址,有:表示有ipv6地址
    grep -q \. <<<$res && grep -q : <<<$res
}

is_netboot_xyz() {
    [ "$distro" = netboot.xyz ]
}

is_alpine_live() {
    [ "$distro" = alpine ] && [ "$hold" = 1 ]
}

is_have_initrd() {
    ! is_netboot_xyz
}

is_use_firmware() {
    # shellcheck disable=SC2154
    [ "$nextos_distro" = debian ] && ! is_virt
}

is_digit() {
    [[ "$1" =~ ^[0-9]+$ ]]
}

is_port_valid() {
    is_digit "$1" && [ "$1" -ge 1 ] && [ "$1" -le 65535 ]
}

get_host_by_url() {
    cut -d/ -f3 <<<$1
}

get_scheme_and_host_by_url() {
    cut -d/ -f1-3 <<<$1
}

get_function() {
    declare -f "$1"
}

get_function_content() {
    declare -f "$1" | sed '1d;2d;$d'
}

insert_into_file() {
    local file=$1
    local location=$2
    local regex_to_find=$3
    shift 3

    if ! [ -f "$file" ]; then
        error_and_exit "File not found: $file"
    fi

    # 默认 grep -E
    if [ $# -eq 0 ]; then
        set -- -E
    fi

    line_num=$(grep "$@" -n "$regex_to_find" "$file" | cut -d: -f1)

    found_count=$(echo "$line_num" | wc -l)
    if [ ! "$found_count" -eq 1 ]; then
        return 1
    fi

    case "$location" in
    before) line_num=$((line_num - 1)) ;;
    after) ;;
    *) return 1 ;;
    esac

    sed -i "${line_num}r /dev/stdin" "$file"
}

test_url() {
    test_url_real false "$@"
}

test_url_grace() {
    test_url_real true "$@"
}

test_url_real() {
    grace=$1
    url=$2
    expect_types=$3
    var_to_eval=$4
    info test url

    failed() {
        $grace && return 1
        error_and_exit "$@"
    }

    tmp_file=$tmp/img-test

    # TODO: 好像无法识别 nixos 官方源的跳转
    # 有的服务器不支持 range,curl会下载整个文件
    # 所以用 head 限制 1M
    # 过滤 curl 23 错误(head 限制了大小)
    # 也可用 ulimit -f 但好像 cygwin 不支持
    # ${PIPESTATUS[n]} 表示第n个管道的返回值
    echo $url
    for i in $(seq 5 -1 0); do
        if command curl --insecure --connect-timeout 10 -Lfr 0-1048575 "$url" \
            1> >(exec head -c 1048576 >$tmp_file) \
            2> >(exec grep -v 'curl: (23)' >&2); then
            break
        else
            ret=$?
            msg="$url not accessible"
            case $ret in
            22)
                # 403 404
                # 这里的 failed 虽然返回 1,但是不会中断脚本,因此要手动 return
                failed "$msg"
                return "$ret"
                ;;
            23)
                # 限制了空间
                break
                ;;
            *)
                # 其他错误
                if [ $i -eq 0 ]; then
                    failed "$msg"
                    return "$ret"
                fi
                ;;
            esac
            sleep 1
        fi
    done

    # 如果要检查文件类型
    if [ -n "$expect_types" ]; then
        install_pkg file
        real_type=$(file_enhanced $tmp_file)
        echo "File type: $real_type"

        # debian 9 ubuntu 16.04-20.04 可能会将 iso 识别成 raw
        for type in $expect_types $([ "$expect_types" = iso ] && echo raw); do
            if [[ ."$real_type" = *."$type" ]]; then
                # 如果要设置变量
                if [ -n "$var_to_eval" ]; then
                    IFS=. read -r "${var_to_eval?}" "${var_to_eval}_warp" <<<"$real_type"
                fi
                return
            fi
        done

        failed "$url
Expected type: $expect_types
Actually type: $real_type"
    fi
}

fix_file_type() {
    # gzip的mime有很多种写法
    # centos7中显示为 x-gzip,在其他系统中显示为 gzip,可能还有其他
    # 所以不用mime判断
    # https://www.digipres.org/formats/sources/tika/formats/#application/gzip

    # centos 7 上的 file 显示 qcow2 的 mime 为 application/octet-stream
    # file debian-12-genericcloud-amd64.qcow2
    # debian-12-genericcloud-amd64.qcow2: QEMU QCOW Image (v3), 2147483648 bytes
    # file --mime debian-12-genericcloud-amd64.qcow2
    # debian-12-genericcloud-amd64.qcow2: application/octet-stream; charset=binary

    # --extension 不靠谱
    # file -b /reinstall-tmp/img-test --mime-type
    # application/x-qemu-disk
    # file -b /reinstall-tmp/img-test --extension
    # ???

    # 1. 删除,;#
    # DOS/MBR boot sector; partition 1: ...
    # gzip compressed data, was ...
    # # ISO 9660 CD-ROM filesystem data... (有些 file 版本开头输出有井号)

    # 2. 删除开头的空格

    # 3. 删除无意义的单词 POSIX, Unicode, UTF-8, ASCII
    # POSIX tar archive (GNU)
    # Unicode text, UTF-8 text
    # UTF-8 Unicode text, with very long lines
    # ASCII text

    # 4. 下面两种都是 raw
    # DOS/MBR boot sector
    # x86 boot sector; partition 1: ...
    sed -E \
        -e 's/[,;#]//g' \
        -e 's/^[[:space:]]*//' \
        -e 's/(POSIX|Unicode|UTF-8|ASCII)//gi' \
        -e 's/^DOS\/MBR boot sector/raw/i' \
        -e 's/^x86 boot sector/raw/i' \
        -e 's/^Zstandard/zstd/i' \
        -e 's/^UDF/iso/i' \
        -e 's/^Windows imaging \(WIM\) image/wim/i' |
        awk '{print $1}' | to_lower
}

# 不用 file -z,因为
# 1. file -z 只能看透一层
# 2. alpine file -z 无法看透部分镜像(前1M),例如:
# guajibao-win10-ent-ltsc-2021-x64-cn-efi.vhd.gz
# guajibao-win7-sp1-ent-x64-cn-efi.vhd.gz
# win7-ent-sp1-x64-cn-efi.vhd.gz
# 还要注意 centos 7 没有 -Z 只有 -z
file_enhanced() {
    file=$1

    full_type=
    while true; do
        type="$(file -b $file | fix_file_type)"
        full_type="$type.$full_type"
        case "$type" in
        xz | gzip | zstd)
            install_pkg "$type"
            $type -dc <"$file" | head -c 1048576 >"$file.inside"
            mv -f "$file.inside" "$file"
            ;;
        tar)
            install_pkg "$type"
            # 隐藏 gzip: unexpected end of file 提醒
            tar xf "$file" -O 2>/dev/null | head -c 1048576 >"$file.inside"
            mv -f "$file.inside" "$file"
            ;;
        *)
            break
            ;;
        esac
    done
    # shellcheck disable=SC2001
    echo "$full_type" | sed 's/\.$//'
}

add_community_repo_for_alpine() {
    local alpine_ver

    # 先检查原来的repo是不是egde
    if grep -q '^http.*/edge/main$' /etc/apk/repositories; then
        alpine_ver=edge
    else
        alpine_ver=v$(cut -d. -f1,2 >/etc/apk/repositories
    fi
}

is_in_container() {
    { is_have_cmd systemd-detect-virt && systemd-detect-virt -qc; } ||
        [ -d /proc/vz ] ||
        { [ -f /proc/1/environ ] && grep -q container=lxc /proc/1/environ; }
}

# 使用 | del_br ,但返回 del_br 之前返回值
run_with_del_cr() {
    if false; then
        # ash 不支持 PIPESTATUS[n]
        res=$("$@") && ret=0 || ret=$?
        echo "$res" | del_cr
        return $ret
    else
        "$@" | del_cr
        return ${PIPESTATUS[0]}
    fi
}

run_with_del_cr_template() {
    if get_function _$exe >/dev/null; then
        run_with_del_cr _$exe "$@"
    else
        run_with_del_cr command $exe "$@"
    fi
}

wmic() {
    if is_have_cmd wmic; then
        # 如果参数没有 GET,添加 GET,防止以下报错
        # wmic memorychip /format:list
        # 此级别的开关异常。
        has_get=false
        for i in "$@"; do
            # 如果参数有 GET
            if [ "$(to_upper <<<"$i")" = GET ]; then
                has_get=true
                break
            fi
        done

        # 输出为 /format:list 格式
        if $has_get; then
            command wmic "$@" /format:list
        else
            command wmic "$@" get /format:list
        fi
        return
    fi

    # powershell wmi 默认参数
    local namespace='root\cimv2'
    local class=
    local filter=
    local props=

    # namespace
    if [[ "$(to_upper <<<"$1")" = /NAMESPACE* ]]; then
        # 删除引号,删除 \\
        namespace=$(cut -d: -f2 <<<"$1" | sed -e "s/[\"']//g" -e 's/\\\\//g')
        shift
    fi

    # class
    if [[ "$(to_upper <<<"$1")" = PATH ]]; then
        class=$2
        shift 2
    else
        # wmic alias list brief
        case "$(to_lower <<<"$1")" in
        nicconfig) class=Win32_NetworkAdapterConfiguration ;;
        memorychip) class=Win32_PhysicalMemory ;;
        *) class=Win32_$1 ;;
        esac
        shift
    fi

    # filter
    if [[ "$(to_upper <<<"$1")" = WHERE ]]; then
        filter=$2
        shift 2
    fi

    # props
    if [[ "$(to_upper <<<"$1")" = GET ]]; then
        props=$2
        shift 2
    fi

    if ! [ -f "$tmp/wmic.ps1" ]; then
        curl -Lo "$tmp/wmic.ps1" "$confhome/wmic.ps1"
    fi

    # shellcheck disable=SC2046
    powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass \
        -File "$(cygpath -w "$tmp/wmic.ps1")" \
        -Namespace "$namespace" \
        -Class "$class" \
        $([ -n "$filter" ] && echo -Filter "$filter") \
        $([ -n "$props" ] && echo -Properties "$props")
}

is_virt() {
    if [ -z "$_is_virt" ]; then
        if is_in_windows; then
            # https://github.com/systemd/systemd/blob/main/src/basic/virt.c
            # https://sources.debian.org/src/hw-detect/1.159/hw-detect.finish-install.d/08hw-detect/
            vmstr='VMware|Virtual|Virtualization|VirtualBox|VMW|Hyper-V|Bochs|QEMU|KVM|OpenStack|KubeVirt|innotek|Xen|Parallels|BHYVE'
            for name in ComputerSystem BIOS BaseBoard; do
                if wmic $name | grep -Eiw $vmstr; then
                    _is_virt=true
                    break
                fi
            done

            # 用运行 windows ,肯定够内存运行 alpine lts netboot
            # 何况还能停止 modloop

            # 没有风扇和温度信息,大概是虚拟机
            # 阿里云 倚天710 arm 有温度传感器
            # ovh KS-LE-3 没有风扇和温度信息?
            if false && [ -z "$_is_virt" ] &&
                ! wmic /namespace:'\\root\cimv2' PATH Win32_Fan 2>/dev/null | grep -q ^Name &&
                ! wmic /namespace:'\\root\wmi' PATH MSAcpi_ThermalZoneTemperature 2>/dev/null | grep -q ^Name; then
                _is_virt=true
            fi
        else
            # aws t4g debian 11
            # systemd-detect-virt: 为 none,即使装了dmidecode
            # virt-what: 未装 deidecode时结果为空,装了deidecode后结果为aws
            # 所以综合两个命令的结果来判断
            if is_have_cmd systemd-detect-virt && systemd-detect-virt -v; then
                _is_virt=true
            fi

            if [ -z "$_is_virt" ]; then
                # debian 安装 virt-what 不会自动安装 dmidecode,因此结果有误
                install_pkg dmidecode virt-what
                # virt-what 返回值始终是0,所以用是否有输出作为判断
                if [ -n "$(virt-what)" ]; then
                    _is_virt=true
                fi
            fi
        fi

        if [ -z "$_is_virt" ]; then
            _is_virt=false
        fi
        echo "VM: $_is_virt"
    fi
    $_is_virt
}

is_absolute_path() {
    # 检查路径是否以/开头
    # 注意语法和 ash 不同
    [[ "$1" = /* ]]
}

is_cpu_supports_x86_64_v3() {
    # 用 ld.so/cpuid/coreinfo.exe 更准确
    # centos 7 /usr/lib64/ld-linux-x86-64.so.2 没有 --help
    # alpine gcompat /lib/ld-linux-x86-64.so.2 没有 --help

    # https://en.wikipedia.org/wiki/X86-64#Microarchitecture_levels
    # https://learn.microsoft.com/sysinternals/downloads/coreinfo

    # abm = popcnt + lzcnt
    # /proc/cpuinfo 不显示 lzcnt, 可用 abm 代替,但 cygwin 也不显示 abm
    # /proc/cpuinfo 不显示 osxsave, 故用 xsave 代替

    need_flags="avx avx2 bmi1 bmi2 f16c fma movbe xsave"
    had_flags=$(grep -m 1 ^flags /proc/cpuinfo | awk -F': ' '{print $2}')

    for flag in $need_flags; do
        if ! grep -qw $flag <<<"$had_flags"; then
            return 1
        fi
    done
}

assert_cpu_supports_x86_64_v3() {
    if ! is_cpu_supports_x86_64_v3; then
        error_and_exit "Could not install $distro $releasever because the CPU does not support x86-64-v3."
    fi
}

# sr-latn-rs 到 sr-latn
en_us() {
    echo "$lang" | awk -F- '{print $1"-"$2}'

    # zh-hk 可回落到 zh-tw
    if [ "$lang" = zh-hk ]; then
        echo zh-tw
    fi
}

# fr-ca 到 ca
us() {
    # 葡萄牙准确对应 pp
    if [ "$lang" = pt-pt ]; then
        echo pp
        return
    fi
    # 巴西准确对应 pt
    if [ "$lang" = pt-br ]; then
        echo pt
        return
    fi

    echo "$lang" | awk -F- '{print $2}'

    # hk 额外回落到 tw
    if [ "$lang" = zh-hk ]; then
        echo tw
    fi
}

# fr-ca 到 fr-fr
en_en() {
    echo "$lang" | awk -F- '{print $1"-"$1}'

    # en-gb 额外回落到 en-us
    if [ "$lang" = en-gb ]; then
        echo en-us
    fi
}

# fr-ca 到 fr
en() {
    # 巴西/葡萄牙回落到葡萄牙语
    if [ "$lang" = pt-br ] || [ "$lang" = pt-pt ]; then
        echo "pp"
        return
    fi

    echo "$lang" | awk -F- '{print $1}'
}

english() {
    case "$lang" in
    ar-sa) echo Arabic ;;
    bg-bg) echo Bulgarian ;;
    cs-cz) echo Czech ;;
    da-dk) echo Danish ;;
    de-de) echo German ;;
    el-gr) echo Greek ;;
    en-gb) echo Eng_Intl ;;
    en-us) echo English ;;
    es-es) echo Spanish ;;
    es-mx) echo Spanish_Latam ;;
    et-ee) echo Estonian ;;
    fi-fi) echo Finnish ;;
    fr-ca) echo FrenchCanadian ;;
    fr-fr) echo French ;;
    he-il) echo Hebrew ;;
    hr-hr) echo Croatian ;;
    hu-hu) echo Hungarian ;;
    it-it) echo Italian ;;
    ja-jp) echo Japanese ;;
    ko-kr) echo Korean ;;
    lt-lt) echo Lithuanian ;;
    lv-lv) echo Latvian ;;
    nb-no) echo Norwegian ;;
    nl-nl) echo Dutch ;;
    pl-pl) echo Polish ;;
    pt-pt) echo Portuguese ;;
    pt-br) echo Brazilian ;;
    ro-ro) echo Romanian ;;
    ru-ru) echo Russian ;;
    sk-sk) echo Slovak ;;
    sl-si) echo Slovenian ;;
    sr-latn | sr-latn-rs) echo Serbian_Latin ;;
    sv-se) echo Swedish ;;
    th-th) echo Thai ;;
    tr-tr) echo Turkish ;;
    uk-ua) echo Ukrainian ;;
    zh-cn) echo ChnSimp ;;
    zh-hk | zh-tw) echo ChnTrad ;;
    esac
}

parse_windows_image_name() {
    set -- $image_name

    if ! [ "$1" = windows ]; then
        return 1
    fi
    shift

    if [ "$1" = server ]; then
        server=server
        shift
    fi

    version=$1
    shift

    if [ "$1" = r2 ]; then
        version+=" r2"
        shift
    fi

    edition=
    while [ $# -gt 0 ]; do
        case "$1" in
        # windows 10 enterprise n ltsc 2021
        k | n | kn) ;;
        *)
            if [ -n "$edition" ]; then
                edition+=" "
            fi
            edition+="$1"
            ;;
        esac
        shift
    done
}

is_have_arm_version() {
    case "$version" in
    10)
        case "$edition" in
        home | 'home single language' | pro | education | enterprise | 'pro education' | 'pro for workstations') return ;;
        'iot enterprise') return ;;
        # arm ltsc 只有 2021 有 iso
        'enterprise ltsc 2021' | 'iot enterprise ltsc 2021') return ;;
        esac
        ;;
    11) return ;;
    esac
    return 1
}

find_windows_iso() {
    parse_windows_image_name || error_and_exit "--image-name wrong: $image_name"
    if ! { [ "$version" = 8 ] || [ "$version" = 8.1 ]; } && [ -z "$edition" ]; then
        error_and_exit "Edition is not set."
    fi

    if [ -z "$lang" ]; then
        lang=en-us
    fi
    langs="$lang $(en_us) $(us) $(en_en) $(en)"
    langs=$(echo "$langs" | xargs -n 1 | awk '!seen[$0]++')
    full_lang=$(english)

    case "$basearch" in
    x86_64)
        arch_win=x64
        arch_win_vlsc=64bit
        ;;
    aarch64)
        arch_win=arm64
        arch_win_vlsc=arm64
        ;;
    esac

    get_windows_iso_link
}

get_windows_iso_link() {
    get_label_msdn() {
        if [ -n "$server" ]; then
            case "$version" in
            2019 | 2022 | 2025)
                case "$edition" in
                serverstandard | serverstandardcore) echo _ ;;
                serverdatacenter | serverdatacentercore) echo _ ;;
                esac
                ;;
            esac
        else
            case "$version" in
            10)
                case "$edition" in
                home | 'home single language') echo consumer ;;
                pro | enterprise) echo business ;;
                education | 'pro education' | 'pro for workstations')
                    case "$arch_win" in
                    arm64) echo consumer ;;
                    x64) echo business ;; # iso 更小
                    esac
                    ;;
                # iot
                'iot enterprise') echo 'iot enterprise' ;;
                # iot ltsc
                'iot enterprise ltsc 2021') echo "$edition" ;;
                # ltsc
                'enterprise ltsc 2021')
                    # arm64 的 enterprise ltsc 2021 要下载 iot enterprise ltsc 2021 iso
                    case "$arch_win" in
                    arm64) echo 'iot enterprise ltsc 2021' ;;
                    x86 | x64) echo 'enterprise ltsc 2021' ;;
                    esac
                    ;;
                esac
                ;;
            11)
                # arm business iso 都没有 education, pro education, pro for workstations
                # 即使它的名字包含 EDU
                # SW_DVD9_Win_Pro_10_22H2.31_Arm64_English_Pro_Ent_EDU_N_MLF_X24-05074.ISO
                # en-us_windows_11_business_editions_version_25h2_arm64_dvd_8afc9b39.iso
                case "$edition" in
                home | 'home single language') echo consumer ;;
                pro | enterprise) echo business ;;
                education | 'pro education' | 'pro for workstations')
                    case "$arch_win" in
                    arm64) echo consumer ;;
                    x64) echo business ;; # iso 更小
                    esac
                    ;;
                # iot
                'iot enterprise' | 'iot enterprise subscription') echo 'iot enterprise' ;;
                # iot ltsc
                'iot enterprise ltsc 2024' | 'iot enterprise subscription ltsc 2024') echo 'iot enterprise ltsc 2024' ;;
                # ltsc
                'enterprise ltsc 2024')
                    # arm64 的 enterprise ltsc 2024 要下载 iot enterprise ltsc 2024 iso
                    case "$arch_win" in
                    arm64) echo 'iot enterprise ltsc 2024' ;;
                    x64) echo 'enterprise ltsc 2024' ;;
                    esac
                    ;;
                esac
                ;;
            esac
        fi
    }

    get_label_vlsc() {
        case "$version" in
        10 | 11)
            case "$edition" in
            pro | education | enterprise | 'pro education' | 'pro for workstations') echo pro ;;
            esac
            ;;
        2025)
            echo SrvSTDCORE
            ;;
        esac
    }

    # msdl 没有每月发布的 iso
    # msdl 只有 consumer 版本,因此里面的 pro 版本不是 vl 版
    # 8.1 没有每月发布的 iso,因此优先从 msdl 下载
    # win10 22h2 arm 有每月发布的 iso,因此不从 msdl 下载
    # win10/11 ltsc 没有每月发布的 iso,但是 msdl 没有 ltsc 版本
    get_label_msdl() {
        :
    }

    get_page() {
        if [ "$arch_win" = arm64 ]; then
            echo arm
        elif is_ltsc; then
            echo ltsc
        elif [ "$server" = 'server' ]; then
            echo server
        else
            case "$version" in
            10 | 11)
                echo "$version"
                ;;
            esac
        fi
    }

    is_ltsc() {
        grep -Ewq 'ltsb|ltsc' <<<"$edition"
    }

    # 部分 bash 不支持 $() 里面嵌套case,所以定义成函数
    label_msdn=$(get_label_msdn)
    label_msdl=$(get_label_msdl)
    label_vlsc=$(get_label_vlsc)
    page=$(get_page)

    if [ "$page" = server ]; then
        page_url=https://massgrave.dev/windows-server-links
    else
        page_url=https://massgrave.dev/windows_${page}_links
    fi

    info "Find windows iso"
    echo "Version:    $version"
    echo "Edition:    $edition"
    echo "Label msdn: $label_msdn"
    echo "Label msdl: $label_msdl"
    echo "Label vlsc: $label_vlsc"
    echo "List:       $page_url"
    echo

    # 先判断是否能自动查找该版本
    # 再判断是否支持 arm
    # 这样可以在输入错误 Edition 时例如 windows 11 enterprise ltsc 2021
    # 显示名称错误,而不是显示该版本不支持 arm

    if [ -z "$page" ] || { [ -z "$label_msdn" ] && [ -z "$label_msdl" ] && [ -z "$label_vlsc" ]; }; then
        error_and_exit "Not support find this iso. Check if --image-name is wrong. Or set --iso manually."
    fi

    if [ "$basearch" = aarch64 ] && ! is_have_arm_version; then
        error_and_exit "No ARM iso for this Windows Version or Edition."
    fi

    if [ -n "$label_msdl" ]; then
        iso=$(curl -L "$page_url" | grep -ioP 'https://[^ ]+?#[0-9]+' | head -1 | grep .)
    else
        http_to_host=$(get_scheme_and_host_by_url "$page_url")
        http_to_current_dir=$(dirname "$page_url")
        curl -L "$page_url" |
            tr -d '\n' | sed -e 's,,\n,g' | # 使每个  占一行
            grep -Ei '\.(iso|img)$' |                              # 找出是 iso 或 img 的行
            # 提取文件名和链接
            # 如果链接是 / 开头,则补全域名
            # 如果链接非 https:// 开头,则补全域名和目录
            sed -E -e 's,(.+),\2 \1,' \
                -e "s, (/), $http_to_host\1," |
            awk '{if ($2 !~ /^https?:\/\//) $2 = "'$http_to_current_dir/'" $2; print}' >$tmp/win.list

        # 如果不是 ltsc ,应该先去除 ltsc 链接,否则最终链接有 ltsc 的
        # 例如查找 windows 10 iot enterprise,会得到
        # en-us_windows_10_iot_enterprise_ltsc_2021_arm64_dvd_e8d4fc46.iso
        # en-us_windows_10_iot_enterprise_version_22h2_arm64_dvd_39566b6b.iso
        # sed -Ei 和 sed -iE 是不同的
        if is_ltsc; then
            sed -Ei '/ltsc|ltsb/!d' $tmp/win.list
        else
            sed -Ei '/ltsc|ltsb/d' $tmp/win.list
        fi

        get_windows_iso_link_inner
    fi
}

get_shortest_line() {
    awk '(NR == 1 || length($0) < length(shortest)) { shortest = $0 } END { print shortest }'
}

get_shortest_line_by_field() {
    local field=$1
    awk "(NR == 1 || length(\$$field) < length(field)) { line = \$0; field = \$$field } END { print line }"
}

get_windows_iso_link_inner() {
    regexs=()

    # msdn
    if [ -n "$label_msdn" ]; then
        if [ "$label_msdn" = _ ]; then
            label_msdn=
        fi
        for lang in $langs; do
            regex=
            for i in ${lang} windows ${server} ${version} ${label_msdn}; do
                if [ -n "$i" ]; then
                    regex+="${i}_"
                fi
            done
            regex+=".*${arch_win}.*.(iso|img)"
            regexs+=("$regex")
        done
    fi

    # vlsc
    # SW_DVD5_Win_10_IOT_Enterprise_2015_LTSB_64Bit_EMB_English_OEM_X20-20063.IMG
    # SW_DVD9_Win_Pro_10_22H2.15_Arm64_English_Pro_Ent_EDU_N_MLF_X23-67223.ISO
    # SWDVD9_WinSrvSTDCORE2025_24H2.16_64Bit_English_DC_STD_MLF_RTMUpdJan26_X24-26760.iso

    # 先判断 full_lang 是否为空
    # 因为假如用户输入的 lang 不正确,full_lang 就为空,正则表达式就无法只匹配当前语言
    if [ -n "$label_vlsc" ] && [ -n "$full_lang" ]; then
        regex="sw_?dvd[59]_win_?${label_vlsc}_?${version}.*${arch_win_vlsc}_${full_lang}.*.(iso|img)"
        regexs+=("$regex")
    fi

    # 查找
    for regex in "${regexs[@]}"; do
        regex=${regex// /_}

        echo "looking for: $regex" >&2
        if line=$(grep -Ei "^$regex " "$tmp/win.list" | get_shortest_line_by_field 1 | grep .) &&
            iso=$(awk '{print $2}' <<<"$line" | grep .); then
            echo "Selected: $line" >&2
            return
        fi
    done

    error_and_exit "Could not find iso for this windows edition or language."
}

setos() {
    local step=$1
    local distro=$2
    local releasever=$3
    info set $step $distro $releasever

    setos_netboot.xyz() {
        if is_efi; then
            if [ "$basearch" = aarch64 ]; then
                eval ${step}_efi=https://boot.netboot.xyz/ipxe/netboot.xyz-arm64.efi
            else
                eval ${step}_efi=https://boot.netboot.xyz/ipxe/netboot.xyz.efi
            fi
        else
            eval ${step}_vmlinuz=https://boot.netboot.xyz/ipxe/netboot.xyz.lkrn
        fi
    }

    setos_alpine() {
        is_virt && flavour=virt || flavour=lts

        # 不要用https 因为甲骨文云arm initramfs阶段不会从硬件同步时钟,导致访问https出错
        if is_in_china; then
            mirror=http://mirror.nju.edu.cn/alpine/v$releasever
        else
            mirror=http://dl-cdn.alpinelinux.org/alpine/v$releasever
        fi
        eval ${step}_vmlinuz=$mirror/releases/$basearch/netboot/vmlinuz-$flavour
        eval ${step}_initrd=$mirror/releases/$basearch/netboot/initramfs-$flavour
        eval ${step}_modloop=$mirror/releases/$basearch/netboot/modloop-$flavour
        eval ${step}_repo=$mirror/main
    }

    setos_debian() {
        is_debian_elts() {
            [ "$releasever" -le 10 ]
        }

        if [ "$releasever" -le 9 ] && [ "$basearch" = aarch64 ]; then
            error_and_exit "Debian $releasever ELTS does not support aarch64."
        fi

        # 用此标记要是否 elts, 用于安装后修改 elts/etls-cn 源
        # shellcheck disable=SC2034
        is_debian_elts && elts=1 || elts=0

        case "$releasever" in
        9) codename=stretch ;;
        10) codename=buster ;;
        11) codename=bullseye ;;
        12) codename=bookworm ;;
        13) codename=trixie ;;
        14) codename=forky ;;
        15) codename=duke ;;
        esac

        if ! is_use_cloud_image && is_debian_elts && is_in_china; then
            warn "
Due to the lack of Debian Freexian ELTS instaler mirrors in China, the installation time may be longer.
Continue?

由于没有 Debian Freexian ELTS 国内安装源,安装时间可能会比较长。
继续安装?
"
            read -r -p '[y/N]: '
            if ! [[ "$REPLY" = [Yy] ]]; then
                exit
            fi
        fi

        # udeb_mirror 安装时的源
        # deb_mirror 安装后要修改成的源
        if is_debian_elts; then
            if is_in_china; then
                # https://github.com/tuna/issues/issues/1999
                # nju 也没同步
                udeb_mirror=deb.freexian.com/extended-lts
                deb_mirror=mirror.nju.edu.cn/debian-elts
                initrd_mirror=mirror.nju.edu.cn/debian-archive/debian
            else
                # 按道理不应该用官方源,但找不到其他源
                udeb_mirror=deb.freexian.com/extended-lts
                deb_mirror=deb.freexian.com/extended-lts
                initrd_mirror=archive.debian.org/debian
            fi
        else
            if is_in_china; then
                # ftp.cn.debian.org 不在国内还严重丢包
                # https://www.itdog.cn/ping/ftp.cn.debian.org
                mirror=mirror.nju.edu.cn/debian
            else
                mirror=deb.debian.org/debian # fastly
            fi
            udeb_mirror=$mirror
            deb_mirror=$mirror
            initrd_mirror=$mirror
        fi

        # 云镜像和 firmware 下载源
        if is_in_china; then
            cdimage_mirror=https://mirror.nju.edu.cn/debian-cdimage
        else
            cdimage_mirror=https://cdimage.debian.org/images # 在瑞典,不是 cdn
            # cloud.debian.org 同样在瑞典,不是 cdn
        fi

        is_virt && flavour=-cloud || flavour=
        # debian 10 云内核 vultr efi vnc 没有显示
        [ "$releasever" -le 10 ] && flavour=
        # 甲骨文 arm64 cloud 内核 vnc 没有显示
        [ "$basearch_alt" = arm64 ] && flavour=

        if is_use_cloud_image; then
            # cloud image
            # https://salsa.debian.org/cloud-team/debian-cloud-images/-/tree/master/config_space/bookworm/files/etc/default/grub.d
            # cloud 包括各种奇怪的优化,例如不显示 grub 菜单
            # 因此使用 nocloud
            if false; then
                is_virt && ci_type=genericcloud || ci_type=generic
            else
                ci_type=nocloud
            fi
            eval ${step}_img=$cdimage_mirror/cloud/$codename/latest/debian-$releasever-$ci_type-$basearch_alt.qcow2
        else
            # 传统安装
            initrd_dir=dists/$codename/main/installer-$basearch_alt/current/images/netboot/debian-installer/$basearch_alt

            eval ${step}_udeb_mirror=$udeb_mirror
            eval ${step}_vmlinuz=https://$initrd_mirror/$initrd_dir/linux
            eval ${step}_initrd=https://$initrd_mirror/$initrd_dir/initrd.gz
            eval ${step}_ks=$confhome/debian.cfg
            eval ${step}_firmware=$cdimage_mirror/unofficial/non-free/firmware/$codename/current/firmware.cpio.gz
            eval ${step}_codename=$codename
        fi

        # 官方安装和云镜像都会用到的
        eval ${step}_deb_mirror=$deb_mirror
        eval ${step}_kernel=linux-image$flavour-$basearch_alt
    }

    setos_kali() {
        if is_use_cloud_image; then
            :
        else
            # 传统安装
            if is_in_china; then
                hostname=mirror.nju.edu.cn
            else
                # http.kali.org 没有 ipv6 地址
                # http.kali.org (geoip 重定向) 到 kali.download (cf)
                hostname=kali.download
            fi
            codename=kali-rolling
            mirror=http://$hostname/kali/dists/$codename/main/installer-$basearch_alt/current/images/netboot/debian-installer/$basearch_alt

            is_virt && flavour=-cloud || flavour=

            eval ${step}_vmlinuz=$mirror/linux
            eval ${step}_initrd=$mirror/initrd.gz
            eval ${step}_ks=$confhome/debian.cfg
            eval ${step}_deb_mirror=$hostname/kali
            eval ${step}_udeb_mirror=$hostname/kali
            eval ${step}_codename=$codename
            eval ${step}_kernel=linux-image$flavour-$basearch_alt
            # 缺少 firmware 下载
        fi
    }

    setos_ubuntu() {
        case "$releasever" in
        16.04) codename=xenial ;;
        18.04) codename=bionic ;;
        20.04) codename=focal ;;
        22.04) codename=jammy ;;
        24.04) codename=noble ;;
        25.10) codename=questing ;; # non-lts
        esac

        if is_use_cloud_image; then
            # cloud image
            if is_in_china; then
                # 有的源没有 releases 镜像
                # https://mirrors.tuna.tsinghua.edu.cn/ubuntu-cloud-images/releases/
                #   https://unicom.mirrors.ustc.edu.cn/ubuntu-cloud-images/releases/
                #            https://mirror.nju.edu.cn/ubuntu-cloud-images/releases/

                # mirrors.cloud.tencent.com
                ci_mirror=https://mirror.nju.edu.cn/ubuntu-cloud-images
            else
                ci_mirror=https://cloud-images.ubuntu.com
            fi

            # 以下版本有 minimal 镜像
            # amd64 所有
            # arm64 24.04 和以上
            is_have_minimal_image() {
                [ "$basearch_alt" = amd64 ] || [ "${releasever%.*}" -ge 24 ]
            }

            get_suffix() {
                if [ "$releasever" = 16.04 ]; then
                    if is_efi; then
                        echo -uefi1
                    else
                        echo -disk1
                    fi
                fi
            }

            if [ "$minimal" = 1 ]; then
                if ! is_have_minimal_image; then
                    error_and_exit "Minimal cloud image is not available for $releasever $basearch_alt."
                fi
                eval ${step}_img="$ci_mirror/minimal/releases/$codename/release/ubuntu-$releasever-minimal-cloudimg-$basearch_alt$(get_suffix).img"
            else
                # 用 codename 而不是 releasever,可减少一次跳转
                eval ${step}_img="$ci_mirror/releases/$codename/release/ubuntu-$releasever-server-cloudimg-$basearch_alt$(get_suffix).img"
            fi
        else
            # 传统安装
            if is_in_china; then
                case "$basearch" in
                "x86_64") mirror=https://mirror.nju.edu.cn/ubuntu-releases/$releasever ;;
                "aarch64") mirror=https://mirror.nju.edu.cn/ubuntu-cdimage/releases/$releasever/release ;;
                esac
            else
                case "$basearch" in
                "x86_64") mirror=https://releases.ubuntu.com/$releasever ;;
                "aarch64") mirror=https://cdimage.ubuntu.com/releases/$releasever/release ;;
                esac
            fi

            # iso
            filename=$(curl -L $mirror/ | grep -oP "ubuntu-$releasever.*?-live-server-$basearch_alt.iso" |
                sort -uV | tail -1 | grep .)
            iso=$mirror/$filename
            # 在 ubuntu 20.04 上,file 命令检测 ubuntu 22.04 iso 结果是 DOS/MBR boot sector
            test_url "$iso" iso
            eval ${step}_iso=$iso

            # ks
            eval ${step}_ks=$confhome/ubuntu.yaml
            eval ${step}_minimal=$minimal
        fi
    }

    setos_arch() {
        if [ "$basearch" = "x86_64" ]; then
            if is_in_china; then
                mirror=https://mirror.nju.edu.cn/archlinux
            else
                mirror=https://geo.mirror.pkgbuild.com # geoip
            fi
        else
            if is_in_china; then
                mirror=https://mirror.nju.edu.cn/archlinuxarm
            else
                # https 证书有问题
                mirror=http://mirror.archlinuxarm.org # geoip
            fi
        fi

        if is_use_cloud_image; then
            # cloud image
            eval ${step}_img=$mirror/images/latest/Arch-Linux-x86_64-cloudimg.qcow2
        else
            # 传统安装
            case "$basearch" in
            x86_64) dir="core/os/$basearch" ;;
            aarch64) dir="$basearch/core" ;;
            esac
            test_url $mirror/$dir/core.db gzip
            eval ${step}_mirror=$mirror
        fi
    }

    setos_nixos() {
        if is_in_china; then
            mirror=https://mirror.nju.edu.cn/nix-channels
        else
            mirror=https://nixos.org/channels
        fi

        if is_use_cloud_image; then
            :
        else
            # 传统安装
            # 该服务器文件缓存 miss 时会响应 206 + Location 头
            # 但 curl 这种情况不会重定向,所以添加 text 类型让它不要报错
            test_url $mirror/nixos-$releasever/store-paths.xz 'xz text'
            eval ${step}_mirror=$mirror
        fi
    }

    setos_gentoo() {
        if is_in_china; then
            mirror=https://mirror.nju.edu.cn/gentoo
        else
            mirror=https://distfiles.gentoo.org # cdn77
        fi

        dir=releases/$basearch_alt/autobuilds

        if is_use_cloud_image; then
            # 使用 systemd 且没有 cloud-init
            prefix=di-$basearch_alt-console
            filename=$(curl -L $mirror/$dir/latest-$prefix.txt | grep '.qcow2' | awk '{print $1}' | grep .)
            file=$mirror/$dir/$filename
            test_url "$file" 'qemu'
            eval ${step}_img=$file
        else
            prefix=stage3-$basearch_alt-systemd
            filename=$(curl -L $mirror/$dir/latest-$prefix.txt | grep '.tar.xz' | awk '{print $1}' | grep .)
            file=$mirror/$dir/$filename
            test_url "$file" 'tar.xz'
            eval ${step}_img=$file
        fi
    }

    setos_opensuse() {
        # https://download.opensuse.org/
        # curl 会跳转到最近的镜像源,但可能会被镜像源 block
        # aria2 会跳转使用 metalink

        # https://downloadcontent.opensuse.org    # 德国
        # https://downloadcontentcdn.opensuse.org # fastly cdn

        # 很多国内源缺少 aarch64 tumbleweed appliances
        #                 https://download.opensuse.org/ports/aarch64/tumbleweed/appliances/
        #          https://mirrors.ustc.edu.cn/opensuse/ports/aarch64/tumbleweed/appliances/
        # https://mirrors.tuna.tsinghua.edu.cn/opensuse/ports/aarch64/tumbleweed/appliances/

        if is_in_china; then
            mirror=https://mirror.nju.edu.cn/opensuse
        else
            mirror=https://downloadcontentcdn.opensuse.org
        fi

        if [ "$releasever" = tumbleweed ]; then
            # tumbleweed
            if [ "$basearch" = aarch64 ]; then
                dir=ports/aarch64/tumbleweed/appliances
            else
                dir=tumbleweed/appliances
            fi
            file=openSUSE-Tumbleweed-Minimal-VM.$basearch-Cloud.qcow2
        else
            # leap
            dir=distribution/leap/$releasever/appliances
            case "$releasever" in
            15.6) file=openSUSE-Leap-$releasever-Minimal-VM.$basearch-Cloud.qcow2 ;;
            16.0) file=Leap-$releasever-Minimal-VM.$basearch-Cloud.qcow2 ;;
            # 16.0) file=Leap-$releasever-Minimal-VM.$basearch-kvm$(if [ "$basearch" = x86_64 ]; then echo '-and-xen'; fi).qcow2 ;;
            esac

            # https://src.opensuse.org/openSUSE/Leap-Images/src/branch/leap-16.0/kiwi-templates-Minimal/Minimal.kiwi
            # https://build.opensuse.org/projects/Virtualization:Appliances:Images:openSUSE-Leap-15.6/packages/kiwi-templates-Minimal/files/Minimal.kiwi
            # https://build.opensuse.org/projects/Virtualization:Appliances:Images:openSUSE-Tumbleweed/packages/kiwi-templates-Minimal/files/Minimal.kiwi
            # 有专门的kvm镜像,openSUSE-Leap-15.5-Minimal-VM.x86_64-kvm-and-xen.qcow2,里面没有cloud-init
            # file=openSUSE-Leap-15.5-Minimal-VM.x86_64-kvm-and-xen.qcow2
        fi
        eval ${step}_img=$mirror/$dir/$file
    }

    setos_windows() {
        auto_find_iso=false
        if [ -z "$iso" ]; then
            auto_find_iso=true
            # 查找时将 windows longhorn serverdatacenter 改成 windows server 2008 serverdatacenter
            image_name=${image_name/windows longhorn server/windows server 2008 server}
            echo "iso url is not set. Attempting to find it automatically."
            find_windows_iso
        fi

        # 将上面的 windows server 2008 serverdatacenter 改回 windows longhorn serverdatacenter
        # 也能纠正用户输入了 windows server 2008 serverdatacenter
        # 注意 windows server 2008 r2 serverdatacenter 不用改
        image_name=${image_name/windows server 2008 server/windows longhorn server}

        if [[ "$iso" = magnet:* ]]; then
            : # 不测试磁力链接
        else
            iso_is_tested=false
            if $auto_find_iso; then
                if test_url_grace "$iso" iso 2>/dev/null; then
                    iso_is_tested=true
                else
                    # 需要用户输入 massgrave.dev 直链
                    info "Set Direct link"
                    # MobaXterm 不支持
                    # printf '\e]8;;http://example.com\e\\This is a link\e]8;;\e\\\n'

                    # MobaXterm 不显示为超链接
                    # info false "请在浏览器中打开 $iso 获取直链并粘贴到这里。"
                    # info false "Please open $iso in browser to get the direct link and paste it here."

                    echo "请在浏览器中打开 $iso 获取直链并粘贴到这里。"
                    echo "Please open $iso in browser to get the direct link and paste it here."
                    IFS= read -r -p "Direct Link: " iso
                    if [ -z "$iso" ]; then
                        error_and_exit "ISO Link is empty."
                    fi
                fi
            fi

            if ! $iso_is_tested; then
                test_url "$iso" iso
            fi

            # 判断 iso 架构是否兼容
            # https://gitlab.com/libosinfo/osinfo-db/-/tree/main/data/os/microsoft.com?ref_type=heads
            # uupdump linux 下合成的标签是 ARM64,windows下合成的标签是 A64
            if file -b "$tmp/img-test" | grep -Eq '_(A64|ARM64)'; then
                iso_arch=arm64
            else
                iso_arch=x86_or_x64
            fi

            if ! {
                { [ "$basearch" = x86_64 ] && [ "$iso_arch" = x86_or_x64 ]; } ||
                    { [ "$basearch" = aarch64 ] && [ "$iso_arch" = arm64 ]; }
            }; then
                warn "
The current machine is $basearch, but it seems the ISO is for $iso_arch. Continue?
当前机器是 $basearch,但 ISO 似乎是 $iso_arch。继续安装?"
                read -r -p '[y/N]: '
                if ! [[ "$REPLY" = [Yy] ]]; then
                    exit
                fi
            fi
        fi

        [ -n "$boot_wim" ] && test_url "$boot_wim" 'wim'

        eval "${step}_iso='$iso'"
        eval "${step}_boot_wim='$boot_wim'"
        eval "${step}_image_name='$image_name'"
    }

    # shellcheck disable=SC2154
    setos_dd() {
        # raw 包含 vhd
        test_url $img 'raw raw.gzip raw.xz raw.zstd raw.tar.gzip raw.tar.xz raw.tar.zstd' img_type

        if is_efi; then
            install_pkg hexdump

            # openwrt 镜像 efi part type 不是 esp
            # 因此改成检测 fat?
            # https://downloads.openwrt.org/releases/23.05.3/targets/x86/64/openwrt-23.05.3-x86-64-generic-ext4-combined-efi.img.gz

            # od 在 coreutils 里面,好像要配合 tr 才能删除空格
            # hexdump 在 util-linux / bsdmainutils 里面
            # xxd 要单独安装,el 在 vim-common 里面
            # xxd -l $((34 * 4096)) -ps -c 128

            # 仅打印前34个扇区 * 4096字节(按最大的算)
            # 每行128字节
            hexdump -n $((34 * 4096)) -e '128/1 "%02x" "\n"' -v "$tmp/img-test" >$tmp/img-test-hex
            if grep -q '^28732ac11ff8d211ba4b00a0c93ec93b' $tmp/img-test-hex; then
                echo 'DD: Image is EFI.'
            else
                echo 'DD: Image is not EFI.'
                warn '
The current machine uses EFI boot, but the DD image seems not an EFI image.
Continue with DD?
当前机器使用 EFI 引导,但 DD 镜像可能不是 EFI 镜像。
继续 DD?'
                read -r -p '[y/N]: '
                if [[ "$REPLY" = [Yy] ]]; then
                    eval ${step}_confirmed_no_efi=1
                else
                    exit
                fi
            fi
        fi
        eval "${step}_img='$img'"
        eval "${step}_img_type='$img_type'"
        eval "${step}_img_type_warp='$img_type_warp'"
    }

    setos_fnos() {
        # 系统盘大小
        min=8
        default=8
        echo "请输入系统分区大小,最小 $min GB,但可能无法更新系统。"
        echo "Please input System Partition Size. Minimal is $min GB but may not be able to do system updates."
        while true; do
            IFS= read -r -p "Size in GB [$default]: " input
            input=${input:-$default}
            if ! { is_digit "$input" && [ "$input" -ge "$min" ]; }; then
                error "Invalid Size. Please Try again."
            else
                eval "${step}_fnos_part_size=${input}G"
                break
            fi
        done

        if [ -z "$iso" ]; then
            # 对于同一行有多个成功匹配,grep -m1 无效
            iso=$(curl -L "https://fnnas.com/download$([ "$basearch" = aarch64 ] && echo -arm)" |
                grep -o 'https://[^"]*\.iso' | head -1 | grep .)

            # curl 7.82.0+
            # curl -L --json '{"url":"'$iso'"}' https://www.fnnas.com/api/download-sign

            iso=$(curl -L \
                -d '{"url":"'$iso'"}' \
                -H 'Content-Type: application/json' \
                https://www.fnnas.com/api/download-sign |
                grep -o 'https://[^"]*')
        fi

        test_url "$iso" iso
        eval "${step}_iso='$iso'"
    }

    setos_aosc() {
        if is_in_china; then
            mirror=https://mirror.nju.edu.cn/anthon/aosc-os
        else
            # 服务器在香港
            mirror=https://releases.aosc.io
        fi

        dir=os-$basearch_alt/base
        file=$(curl -L $mirror/$dir/ | grep -oP 'aosc-os_base_.*?\.tar.xz' |
            sort -uV | tail -1 | grep .)
        img=$mirror/$dir/$file
        test_url $img 'tar.xz'
        eval ${step}_img=$img
    }

    setos_centos_almalinux_rocky_fedora() {
        # el 10 需要 x86-64-v3,除了 almalinux
        if [ "$basearch" = x86_64 ] &&
            { [ "$distro" = centos ] || [ "$distro" = rocky ]; } &&
            [ "$releasever" -ge 10 ]; then
            assert_cpu_supports_x86_64_v3
        fi

        elarch=$basearch
        if [ "$basearch" = x86_64 ] &&
            [ "$distro" = almalinux ] && [ "$releasever" -ge 10 ] &&
            ! is_cpu_supports_x86_64_v3; then
            elarch=x86_64_v2
        fi

        if is_use_cloud_image; then
            # ci
            if is_in_china; then
                case $distro in
                centos) ci_mirror="https://mirror.nju.edu.cn/centos-cloud/centos" ;;
                almalinux) ci_mirror="https://mirror.nju.edu.cn/almalinux/$releasever/cloud/$elarch/images" ;;
                rocky) ci_mirror="https://mirror.nju.edu.cn/rocky/$releasever/images/$elarch" ;;
                fedora) ci_mirror="https://mirror.nju.edu.cn/fedora/releases/$releasever/Cloud/$elarch/images" ;;
                esac
            else
                case $distro in
                centos) ci_mirror="https://cloud.centos.org/centos" ;;
                almalinux) ci_mirror="https://repo.almalinux.org/almalinux/$releasever/cloud/$elarch/images" ;;
                rocky) ci_mirror="https://download.rockylinux.org/pub/rocky/$releasever/images/$elarch" ;;
                fedora) ci_mirror="https://d2lzkl7pfhq30w.cloudfront.net/pub/fedora/linux/releases/$releasever/Cloud/$elarch/images" ;;
                esac
            fi
            case $distro in
            centos)
                case $releasever in
                7)
                    # CentOS-7-aarch64-GenericCloud.qcow2c 是旧版本
                    ver=-2211
                    ci_image=$ci_mirror/$releasever/images/CentOS-$releasever-$elarch-GenericCloud$ver.qcow2c
                    ;;
                *)
                    # 有 bios 和 efi 镜像
                    # https://cloud.centos.org/centos/10-stream/x86_64/images/CentOS-Stream-GenericCloud-10-latest.x86_64.qcow2
                    # https://cloud.centos.org/centos/10-stream/x86_64/images/CentOS-Stream-GenericCloud-x86_64-10-latest.x86_64.qcow2
                    [ "$elarch" = x86_64 ] &&
                        ci_image=$ci_mirror/$releasever-stream/$elarch/images/CentOS-Stream-GenericCloud-x86_64-$releasever-latest.$elarch.qcow2 ||
                        ci_image=$ci_mirror/$releasever-stream/$elarch/images/CentOS-Stream-GenericCloud-$releasever-latest.$elarch.qcow2
                    ;;
                esac
                ;;
            almalinux) ci_image=$ci_mirror/AlmaLinux-$releasever-GenericCloud-latest.$elarch.qcow2 ;;
            rocky) ci_image=$ci_mirror/Rocky-$releasever-GenericCloud-Base.latest.$elarch.qcow2 ;;
            fedora)
                # 不加 / 会跳转到 https://dl.fedoraproject.org,纯 ipv6 无法访问
                # curl -L -6 https://d2lzkl7pfhq30w.cloudfront.net/pub/fedora/linux/releases/42/Cloud/x86_64/images
                # curl -L -6 https://d2lzkl7pfhq30w.cloudfront.net/pub/fedora/linux/releases/42/Cloud/x86_64/images/
                filename=$(curl -L $ci_mirror/ | grep -oP "Fedora-Cloud-Base-Generic.*?.qcow2" |
                    sort -uV | tail -1 | grep .)
                ci_image=$ci_mirror/$filename
                ;;
            esac

            eval ${step}_img=${ci_image}
        else
            # 传统安装
            case $distro in
            centos) mirrorlist="https://mirrors.centos.org/mirrorlist?repo=centos-baseos-$releasever-stream&arch=$elarch" ;;
            almalinux) mirrorlist="https://mirrors.almalinux.org/mirrorlist/$releasever/baseos" ;;
            rocky) mirrorlist="https://mirrors.rockylinux.org/mirrorlist?arch=$elarch&repo=BaseOS-$releasever" ;;
            fedora) mirrorlist="https://mirrors.fedoraproject.org/mirrorlist?arch=$elarch&repo=fedora-$releasever" ;;
            esac

            # rocky/centos9 需要删除第一行注释, almalinux 需要替换链接里面的 $basearch
            for cur_mirror in $(curl -L $mirrorlist | sed "/^#/d" | sed "s,\$basearch,$elarch,"); do
                host=$(get_host_by_url $cur_mirror)
                if is_host_has_ipv4_and_ipv6 $host &&
                    test_url_grace ${cur_mirror}images/pxeboot/vmlinuz; then
                    mirror=$cur_mirror
                    break
                fi
            done

            if [ -z "$mirror" ]; then
                error_and_exit "All mirror failed."
            fi

            eval "${step}_mirrorlist='${mirrorlist}'"

            eval ${step}_ks=$confhome/redhat.cfg
            eval ${step}_vmlinuz=${mirror}images/pxeboot/vmlinuz
            eval ${step}_initrd=${mirror}images/pxeboot/initrd.img
            eval ${step}_squashfs=${mirror}images/install.img
            test_url ${mirror}images/install.img 'squashfs'
        fi
    }

    setos_oracle() {
        # el 10 需要 x86-64-v3
        if [ "$basearch" = x86_64 ] && [ "$releasever" -ge 10 ]; then
            assert_cpu_supports_x86_64_v3
        fi

        if is_use_cloud_image; then
            # ci
            install_pkg jq
            mirror=https://yum.oracle.com

            [ "$basearch" = aarch64 ] &&
                template_prefix=ol${releasever}_${basearch}-cloud ||
                template_prefix=ol${releasever}
            curl -Lo $tmp/oracle.json $mirror/templates/OracleLinux/$template_prefix-template.json
            dir=$(jq -r .base_url $tmp/oracle.json)
            file=$(jq -r .kvm.image $tmp/oracle.json)
            ci_image=$mirror$dir/$file

            eval ${step}_img=${ci_image}
        else
            :
        fi
    }

    setos_redhat() {
        if is_use_cloud_image; then
            # el 10 需要 x86-64-v3
            if [ "$basearch" = x86_64 ] && [[ "$img" = *rhel-10* ]]; then
                assert_cpu_supports_x86_64_v3
            fi
            eval "${step}_img='$img'"
        else
            :
        fi
    }

    setos_opencloudos() {
        # https://mirrors.opencloudos.tech 不支持 ipv6
        # https://mirrors.cloud.tencent.com 没有 stream
        if [ "$releasever" -ge 23 ]; then
            mirror=https://mirrors.opencloudos.tech/opencloudos-stream/releases
        else
            mirror=https://mirrors.cloud.tencent.com/opencloudos
        fi

        if is_use_cloud_image; then
            # ci
            if [ "$releasever" -eq 9 ]; then
                dir=$releasever/images/qcow2/$basearch
            else
                dir=$releasever/images/$basearch
            fi

            file=$(curl -L $mirror/$dir/ | grep -oP 'OpenCloudOS.*?\.qcow2' |
                sort -uV | tail -1 | grep .)
            eval ${step}_img=$mirror/$dir/$file
        else
            :
        fi
    }

    setos_anolis() {
        mirror=https://mirrors.openanolis.cn/anolis
        if is_use_cloud_image; then
            # ci
            dir=$releasever/isos/GA/$basearch
            [ "$releasever" -ge 23 ] &&
                filename='AnolisOS.*?\.qcow2' ||
                filename='AnolisOS.*?-ANCK\.qcow2'
            file=$(curl -L $mirror/$dir/ | grep -oP "$filename" |
                sort -uV | tail -1 | grep .)
            eval ${step}_img=$mirror/$dir/$file
        else
            :
        fi
    }

    setos_openeuler() {
        if is_in_china; then
            mirror=https://repo.openeuler.openatom.cn
        else
            mirror=https://repo.openeuler.org
        fi
        if is_use_cloud_image; then
            # ci
            name=$(curl -L "$mirror/" | grep -oE "openEuler-$releasever(-LTS)?(-SP[0-9])?" |
                sort -uV | tail -1 | grep .)
            eval ${step}_img=$mirror/$name/virtual_machine_img/$basearch/$name-$basearch.qcow2.xz
        else
            :
        fi
    }

    eval ${step}_distro=$distro
    eval ${step}_releasever=$releasever

    case "$distro" in
    centos | almalinux | rocky | fedora) setos_centos_almalinux_rocky_fedora ;;
    *) setos_$distro ;;
    esac

    # debian/kali <=256M 必须使用云内核,否则不够内存
    if is_distro_like_debian && ! is_in_windows && [ "$ram_size" -le 256 ]; then
        exit_if_cant_use_cloud_kernel
    fi

    # 集中测试云镜像格式
    if is_use_cloud_image && [ "$step" = finalos ]; then
        # shellcheck disable=SC2154
        test_url $finalos_img 'qemu qemu.gzip qemu.xz qemu.zstd raw.xz' finalos_img_type
    fi
}

is_distro_like_redhat() {
    if [ -n "$1" ]; then
        _distro=$1
    else
        _distro=$distro
    fi
    [ "$_distro" = redhat ] || [ "$_distro" = centos ] || [ "$_distro" = almalinux ] || [ "$_distro" = rocky ] || [ "$_distro" = fedora ] || [ "$_distro" = oracle ]
}

is_distro_like_debian() {
    if [ -n "$1" ]; then
        _distro=$1
    else
        _distro=$distro
    fi
    [ "$_distro" = debian ] || [ "$_distro" = kali ]
}

get_latest_distro_releasever() {
    get_function_content verify_os_name |
        grep -wo "$1 [^'\"]*" | awk -F'|' '{print $NF}'
}

# 检查是否为正确的系统名
verify_os_name() {
    if [ -z "$*" ]; then
        usage_and_exit
    fi

    # 不要删除 centos 7
    for os in \
        'centos      7|9|10' \
        'anolis      7|8|23' \
        'opencloudos 8|9|23' \
        'almalinux   8|9|10' \
        'rocky       8|9|10' \
        'oracle      8|9|10' \
        'fnos        1' \
        'fedora      42|43' \
        'nixos       25.11' \
        'debian      9|10|11|12|13' \
        'opensuse    15.6|16.0|tumbleweed' \
        'alpine      3.20|3.21|3.22|3.23' \
        'openeuler   20.03|22.03|24.03|25.09' \
        'ubuntu      16.04|18.04|20.04|22.04|24.04|25.10' \
        'redhat' \
        'kali' \
        'arch' \
        'gentoo' \
        'aosc' \
        'windows' \
        'dd' \
        'netboot.xyz' \
        'reset'; do
        read -r ds vers <<<"$os"
        vers_=${vers//\./\\\.}
        finalos=$(echo "$@" | to_lower | sed -n -E "s,^($ds)[ :-]?(|$vers_)$,\1 \2,p")
        if [ -n "$finalos" ]; then
            read -r distro releasever <<<"$finalos"
            # 默认版本号
            if [ -z "$releasever" ] && [ -n "$vers" ]; then
                releasever=$(awk -F '|' '{print $NF}' <<<"|$vers")
            fi
            return
        fi
    done

    error "Please specify a proper os"
    usage_and_exit
}

verify_os_args() {
    case "$distro" in
    dd) [ -n "$img" ] || error_and_exit "dd need --img" ;;
    redhat) [ -n "$img" ] || error_and_exit "redhat need --img" ;;
    windows) [ -n "$image_name" ] || error_and_exit "Install Windows need --image-name." ;;
    esac

    case "$distro" in
    netboot.xyz | windows) [ -z "$ssh_keys" ] || error_and_exit "not support ssh key for $distro" ;;
    esac
}

get_cmd_path() {
    # arch 云镜像不带 which
    # command -v 包括脚本里面的方法
    # ash 无效
    type -f -p $1
}

is_have_cmd() {
    get_cmd_path $1 >/dev/null 2>&1
}

install_pkg() {
    is_in_windows && return

    find_pkg_mgr() {
        [ -n "$pkg_mgr" ] && return

        # 查找方法1: 通过 ID / ID_LIKE
        # 因为可能装了多种包管理器
        if [ -f /etc/os-release ]; then
            # shellcheck source=/dev/null
            for id in $({ . /etc/os-release && echo $ID $ID_LIKE; }); do
                # https://github.com/chef/os_release
                case "$id" in
                fedora | centos | rhel) is_have_cmd dnf && pkg_mgr=dnf || pkg_mgr=yum ;;
                debian | ubuntu) pkg_mgr=apt-get ;;
                opensuse | suse) pkg_mgr=zypper ;;
                alpine) pkg_mgr=apk ;;
                arch) pkg_mgr=pacman ;;
                gentoo) pkg_mgr=emerge ;;
                nixos) pkg_mgr=nix-env ;;
                esac
                [ -n "$pkg_mgr" ] && return
            done
        fi

        # 查找方法 2
        for mgr in dnf yum apt-get pacman zypper emerge apk nix-env; do
            is_have_cmd $mgr && pkg_mgr=$mgr && return
        done

        return 1
    }

    cmd_to_pkg() {
        unset USE
        case $cmd in
        ar)
            case "$pkg_mgr" in
            *) pkg="binutils" ;;
            esac
            ;;
        xz)
            case "$pkg_mgr" in
            apt-get) pkg="xz-utils" ;;
            *) pkg="xz" ;;
            esac
            ;;
        lsblk | findmnt)
            case "$pkg_mgr" in
            apk) pkg="$cmd" ;;
            *) pkg="util-linux" ;;
            esac
            ;;
        lsmem)
            case "$pkg_mgr" in
            apk) pkg="util-linux-misc" ;;
            *) pkg="util-linux" ;;
            esac
            ;;
        fdisk)
            case "$pkg_mgr" in
            apt-get) pkg="fdisk" ;;
            apk) pkg="util-linux-misc" ;;
            *) pkg="util-linux" ;;
            esac
            ;;
        hexdump)
            case "$pkg_mgr" in
            apt-get) pkg="bsdmainutils" ;;
            *) pkg="util-linux" ;;
            esac
            ;;
        unsquashfs)
            case "$pkg_mgr" in
            zypper) pkg="squashfs" ;;
            emerge) pkg="squashfs-tools" && export USE="lzma" ;;
            *) pkg="squashfs-tools" ;;
            esac
            ;;
        nslookup | dig)
            case "$pkg_mgr" in
            apt-get) pkg="dnsutils" ;;
            pacman) pkg="bind" ;;
            apk | emerge) pkg="bind-tools" ;;
            yum | dnf | zypper) pkg="bind-utils" ;;
            esac
            ;;
        iconv)
            case "$pkg_mgr" in
            apk) pkg="musl-utils" ;;
            *) error_and_exit "Which GNU/Linux do not have iconv built-in?" ;;
            esac
            ;;
        *) pkg=$cmd ;;
        esac
    }

    # 系统                       package名称                                    repo名称
    # centos/alma/rocky/fedora   epel-release                                   epel
    # oracle linux               oracle-epel-release                            ol9_developer_EPEL
    # opencloudos                epol-release                                   EPOL
    # alibaba cloud linux 3      epel-release/epel-aliyuncs-release(qcow2自带)  epel
    # anolis 23                  anolis-epao-release                            EPAO

    # anolis 8
    # [root@localhost ~]# yum search *ep*-release | grep -v next
    # ========================== Name Matched: *ep*-release ==========================
    # anolis-epao-release.noarch : EPAO Packages for Anolis OS 8 repository configuration
    # epel-aliyuncs-release.noarch : Extra Packages for Enterprise Linux repository configuration
    # epel-release.noarch : Extra Packages for Enterprise Linux repository configuration (qcow2自带)

    check_is_need_epel() {
        is_need_epel() {
            case "$pkg" in
            dpkg) true ;;
            jq) is_have_cmd yum && ! is_have_cmd dnf ;; # el7/ol7 的 jq 在 epel 仓库
            *) false ;;
            esac
        }

        get_epel_repo_name() {
            # el7 不支持 yum repolist --all,要使用 yum repolist all
            # el7 yum repolist 第一栏有 /x86_64 后缀,因此要去掉。而 el9 没有
            $pkg_mgr repolist all | awk '{print $1}' | awk -F/ '{print $1}' | grep -Ei 'ep(el|ol|ao)$'
        }

        get_epel_pkg_name() {
            # el7 不支持 yum list --available,要使用 yum list available
            $pkg_mgr list available | grep -E '(.*-)?ep(el|ol|ao)-(.*-)?release' |
                awk '{print $1}' | cut -d. -f1 | grep -v next | head -1
        }

        if is_need_epel; then
            if ! epel=$(get_epel_repo_name); then
                $pkg_mgr install -y "$(get_epel_pkg_name)"
                epel=$(get_epel_repo_name)
            fi
            enable_epel="--enablerepo=$epel"
        else
            enable_epel=
        fi
    }

    install_pkg_real() {
        text="$pkg"
        if [ "$pkg" != "$cmd" ]; then
            text+=" ($cmd)"
        fi
        echo "Installing package '$text'..."

        case $pkg_mgr in
        dnf)
            check_is_need_epel
            dnf install $enable_epel -y --setopt=install_weak_deps=False $pkg
            ;;
        yum)
            check_is_need_epel
            yum install $enable_epel -y $pkg
            ;;
        emerge) emerge --oneshot $pkg ;;
        pacman) pacman -Syu --noconfirm --needed $pkg ;;
        zypper) zypper install -y $pkg ;;
        apk)
            add_community_repo_for_alpine
            apk add $pkg
            ;;
        apt-get)
            [ -z "$apt_updated" ] && apt-get update && apt_updated=1
            DEBIAN_FRONTEND=noninteractive apt-get install -y $pkg
            ;;
        nix-env)
            # 不指定 channel 会很慢,而且很占内存
            [ -z "$nix_updated" ] && nix-channel --update && nix_updated=1
            nix-env -iA nixos.$pkg
            ;;
        esac
    }

    is_need_reinstall() {
        cmd=$1

        # gentoo 默认编译的 unsquashfs 不支持 xz
        if [ "$cmd" = unsquashfs ] && is_have_cmd emerge && ! $cmd |& grep -wq xz; then
            echo "unsquashfs not supported xz. rebuilding."
            return 0
        fi

        # busybox fdisk 无法显示 mbr 分区表的 id
        if [ "$cmd" = fdisk ] && is_have_cmd apk && $cmd |& grep -wq BusyBox; then
            return 0
        fi

        # busybox grep 不支持 -oP
        if [ "$cmd" = grep ] && is_have_cmd apk && $cmd |& grep -wq BusyBox; then
            return 0
        fi

        return 1
    }

    for cmd in "$@"; do
        if ! is_have_cmd $cmd || is_need_reinstall $cmd; then
            if ! find_pkg_mgr; then
                error_and_exit "Can't find compatible package manager. Please manually install $cmd."
            fi
            cmd_to_pkg
            install_pkg_real
        fi
    done >&2
}

is_valid_ram_size() {
    is_digit "$1" && [ "$1" -gt 0 ]
}

check_ram() {
    ram_standard=$(
        case "$distro" in
        netboot.xyz) echo 0 ;;
        alpine | debian | kali | dd) echo 256 ;;
        arch | gentoo | aosc | nixos | windows) echo 512 ;;
        redhat | centos | almalinux | rocky | fedora | oracle | ubuntu | anolis | opencloudos | openeuler) echo 1024 ;;
        opensuse | fnos) echo -1 ;; # 没有安装模式
        esac
    )

    # 不用检查内存的情况
    if [ "$ram_standard" -eq 0 ]; then
        return
    fi

    # 未测试
    ram_cloud_image=256

    has_cloud_image=$(
        case "$distro" in
        redhat | centos | almalinux | rocky | oracle | fedora | debian | ubuntu | opensuse | anolis | openeuler) echo true ;;
        netboot.xyz | alpine | dd | arch | gentoo | nixos | kali | windows) echo false ;;
        esac
    )

    if is_in_windows; then
        ram_size=$(wmic memorychip get capacity | awk -F= '{sum+=$2} END {if(sum>0) print sum/1024/1024}')
    else
        # lsmem最准确但 centos7 arm 和 alpine 不能用,debian 9 util-linux 没有 lsmem
        # arm 24g dmidecode 显示少了128m
        # arm 24g lshw 显示23BiB
        # ec2 t4g arm alpine 用 lsmem 和 dmidecode 都无效,要用 lshw,但结果和free -m一致,其他平台则没问题
        install_pkg lsmem
        ram_size=$(lsmem -b 2>/dev/null | grep 'Total online memory:' | awk '{ print $NF/1024/1024 }')

        if ! is_valid_ram_size "$ram_size"; then
            install_pkg dmidecode
            ram_size=$(dmidecode -t 17 | grep "Size.*[GM]B" | awk '{if ($3=="GB") s+=$2*1024; else s+=$2} END {if(s>0) print s}')
        fi

        if ! is_valid_ram_size "$ram_size"; then
            install_pkg lshw
            # 不能忽略 -i,alpine 显示的是 System memory
            ram_str=$(lshw -c memory -short | grep -i 'System Memory' | awk '{print $3}')
            ram_size=$(grep <<<$ram_str -o '[0-9]*')
            grep <<<$ram_str GiB && ram_size=$((ram_size * 1024))
        fi
    fi

    # 用于兜底,不太准确
    # cygwin 要装 procps-ng 才有 free 命令
    if ! is_valid_ram_size "$ram_size"; then
        ram_size_k=$(grep '^MemTotal:' /proc/meminfo | awk '{print $2}')
        ram_size=$((ram_size_k / 1024 + 64 + 4))
    fi

    if ! is_valid_ram_size "$ram_size"; then
        error_and_exit "Could not detect RAM size."
    fi

    # ram 足够就用普通方法安装,否则如果内存大于512就用 cloud image
    # TODO: 测试 256 384 内存
    if ! is_use_cloud_image && [ $ram_size -lt $ram_standard ]; then
        if $has_cloud_image; then
            info "RAM < $ram_standard MB. Fallback to cloud image mode"
            cloud_image=1
        else
            error_and_exit "Could not install $distro: RAM < $ram_standard MB."
        fi
    fi

    if is_use_cloud_image && [ $ram_size -lt $ram_cloud_image ]; then
        error_and_exit "Could not install $distro using cloud image: RAM < $ram_cloud_image MB."
    fi
}

is_efi() {
    if is_in_windows; then
        # bcdedit | grep -qi '^path.*\.efi'
        mountvol | grep -q -a 'EFI'
    else
        [ -d /sys/firmware/efi ]
    fi
}

is_grub_dir_linked() {
    # cloudcone 重装前/重装后(方法1)
    [ "$(readlink -f /boot/grub/grub.cfg)" = /boot/grub2/grub.cfg ] ||
        [ "$(readlink -f /boot/grub2/grub.cfg)" = /boot/grub/grub.cfg ] ||
        # cloudcone 重装后(方法2)
        { [ -f /boot/grub2/grub.cfg ] && [ "$(cat /boot/grub2/grub.cfg)" = 'chainloader (hd0)+1' ]; }
}

is_secure_boot_enabled() {
    if is_efi; then
        if is_in_windows; then
            reg query 'HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot\State' /v UEFISecureBootEnabled 2>/dev/null | grep 0x1
        else
            if dmesg | grep -i 'Secure boot enabled'; then
                return 0
            fi
            install_pkg mokutil
            mokutil --sb-state 2>&1 | grep -i 'SecureBoot enabled'
        fi
    else
        return 1
    fi
}

is_need_boot_vmlinuz() {
    ! { is_netboot_xyz && is_efi; }
}

# 只有 linux bios 是用本机的 grub/extlinux
is_use_local_grub_extlinux() {
    is_need_boot_vmlinuz && ! is_in_windows && ! is_efi
}

is_use_local_grub() {
    is_use_local_grub_extlinux && is_mbr_using_grub
}

is_use_local_extlinux() {
    is_use_local_grub_extlinux && ! is_mbr_using_grub
}

# 软 raid 时 xda 可能不是引导盘,以后再修正
is_mbr_using_grub() {
    find_main_disk
    # 各发行版不一定自带 strings hexdump xxd od 命令
    head -c 440 /dev/$xda | grep -a -iq 'GRUB'
}

to_upper() {
    tr '[:lower:]' '[:upper:]'
}

to_lower() {
    tr '[:upper:]' '[:lower:]'
}

del_cr() {
    # wmic/reg 换行符是 \r\r\n
    # wmic nicconfig where InterfaceIndex=$id get MACAddress,IPAddress,IPSubnet,DefaultIPGateway | hexdump -c
    sed -E 's/\r+$//'
}

del_empty_lines() {
    sed '/^[[:space:]]*$/d'
}

del_comment_lines() {
    sed '/^[[:space:]]*#/d'
}

trim() {
    # sed -E -e 's/^[[:space:]]+//' -e 's/[[:space:]]+$//'
    sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'
}

prompt_password() {
    info "prompt password"
    warn false "Leave blank to use a random password."
    warn false "不填写则使用随机密码"
    while true; do
        IFS= read -r -p "Password: " password
        if [ -n "$password" ]; then
            IFS= read -r -p "Retype password: " password_confirm
            if [ "$password" = "$password_confirm" ]; then
                break
            else
                error "Passwords don't match. Try again."
            fi
        else
            # 特殊字符列表
            # https://learn.microsoft.com/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/hh994562(v=ws.11)
            # 有的机器运行 centos 7 ,用 /dev/random 产生 16 位密码,开启了 rngd 也要 5 秒,关闭了 rngd 则长期阻塞
            chars=\''A-Za-z0-9~!@#$%^&*_=+`|(){}[]:;"<>,.?/-'
            password=$(tr -dc "$chars" >"$dir/password-plaintext"
    fi

    # sha512
    # 以下系统均支持 sha512 密码,但是生成密码需要不同的工具
    # 兼容性     openssl   mkpasswd          busybox  python
    # centos 7     ×      只有expect的       需要编译    √
    # centos 8     √      只有expect的
    # debian 9     ×         √
    # ubuntu 16    ×         √
    # alpine       √      可能系统装了expect     √
    # cygwin       √
    # others       √

    # alpine
    if is_have_cmd busybox && busybox mkpasswd --help 2>&1 | grep -wq sha512; then
        crypted=$(printf '%s' "$password" | busybox mkpasswd -m sha512)
    # others
    elif install_pkg openssl && openssl passwd --help 2>&1 | grep -wq '\-6'; then
        crypted=$(printf '%s' "$password" | openssl passwd -6 -stdin)
    # debian 9 / ubuntu 16
    elif is_have_cmd apt-get && install_pkg whois && mkpasswd -m help | grep -wq sha-512; then
        crypted=$(printf '%s' "$password" | mkpasswd -m sha-512 --stdin)
    # centos 7
    # crypt.mksalt 是 python3 的
    # 红帽把它 backport 到了 centos7 的 python2 上
    # 在其它发行版的 python2 上运行会出错
    elif is_have_cmd yum && is_have_cmd python2; then
        crypted=$(python2 -c "import crypt, sys; print(crypt.crypt(sys.argv[1], crypt.mksalt(crypt.METHOD_SHA512)))" "$password")
    else
        error_and_exit "Could not generate sha512 password."
    fi
    echo "$crypted" >"$dir/password-linux-sha512"

    # yescrypt
    # 旧系统不支持,先不管
    if false; then
        if mkpasswd -m help | grep -wq yescrypt; then
            crypted=$(printf '%s' "$password" | mkpasswd -m yescrypt --stdin)
            echo "$crypted" >"$dir/password-linux-yescrypt"
        fi
    fi

    # windows
    if [ "$distro" = windows ] || [ "$distro" = dd ]; then
        install_pkg iconv

        # 要分两行写,因为 echo "$(xxx)" 返回值始终为 0,出错也不会中断脚本
        # grep . 为了保证脚本没有出错
        base64=$(printf '%s' "${password}Password" | iconv -f UTF-8 -t UTF-16LE | base64 -w 0 | grep .)
        echo "$base64" >"$dir/password-windows-user-base64"

        base64=$(printf '%s' "${password}AdministratorPassword" | iconv -f UTF-8 -t UTF-16LE | base64 -w 0 | grep .)
        echo "$base64" >"$dir/password-windows-administrator-base64"
    fi
}

# 记录主硬盘
find_main_disk() {
    if [ -n "$main_disk" ]; then
        return
    fi

    if is_in_windows; then
        # TODO:
        # 已测试 vista
        # 测试 软raid
        # 测试 动态磁盘

        # diskpart 命令结果
        # 磁盘 ID: E5FDE61C
        # 磁盘 ID: {92CF6564-9B2E-4348-A3BD-D84E3507EBD7}
        main_disk=$(printf "%s\n%s" "select volume $c" "uniqueid disk" | diskpart |
            tail -1 | awk '{print $NF}' | sed 's,[{}],,g')
    else
        if [ -z "$xda" ]; then
            # centos7下测试     lsblk --inverse $mapper | grep -w disk     grub2-probe -t disk /
            # 跨硬盘btrfs       只显示第一个硬盘                            显示两个硬盘
            # 跨硬盘lvm         显示两个硬盘                                显示/dev/mapper/centos-root
            # 跨硬盘软raid      显示两个硬盘                                显示/dev/md127

            # 还有 findmnt

            # 理论上 /boot/efi /efi /boot 可以不在主硬盘上
            # 因此只查找 / 分区

            install_pkg lsblk
            # lvm 显示的是 /dev/mapper/xxx-yyy,再用第二条命令得到sda
            mapper=$(mount | awk '$3=="/" {print $1}' | grep .)
            xdas=$(lsblk -rn --inverse $mapper | grep -w disk | awk '{print $1}' | sort -u | grep .)

            # 注意 wc -l 的坑
            # wc -l <<<"" 输出 1

            # 检测主硬盘是否横跨多个磁盘
            if [ "$(wc -l <<<"$xdas")" -eq 1 ]; then
                xda=$xdas
            else
                # vultr 官网新建数据盘时有写,部分地区的实例,不能从数据盘启动
                # 实测 nvram 可写入数据盘的条目但不生效,手动安装 debian 到此硬盘,nvram 也不生效
                # 从 bios 手动选择数据盘的 efi 文件才能启动
                # 但是 nvram BootNext 数据盘的条目却可以生效

                # vultr 从软 raid 0 debian 下用本脚本安装 debian ,进入 grub efi 时,无法识别系统分区 md1 的文件系统,也少了 hd1
                # 但是从 bios 手动选择 reinstall 的 grub efi 时却可以识别
                # 后期应该将 vmlinux/initrd 放到 efi/boot 分区

                # Gemini 说:
                # 使用 efibootmgr 的 BootNext 时,固件走的是“快速路径”:
                # 它只初始化存放 EFI 引导文件(grubx64.efi)的那块硬盘(hd0),然后直接把控制权交给 GRUB。
                # 因为 hd1 还没“醒”,GRUB 自然无法拼凑出完整的 md1 阵列,导致无法识别文件系统。

                # 使用 nativedisk 可以强制 grub 识别所有硬盘?
                info false "Multiple disks found for root partition:"
                echo '-----'
                printf "%s\n" "$xdas"
                echo '-----'
                read -r -p "Select a disk to install: " xda
                if ! grep -Fqx "$xda" <<<"$xdas"; then
                    error_and_exit "Invalid Input."
                fi
            fi
        fi

        info "Main disk: $xda"

        # 可以用 dd 找出 guid?

        # centos7 blkid lsblk 不显示 PTUUID
        # centos7 sfdisk 不显示 Disk identifier
        # alpine blkid 不显示 gpt 分区表的 PTUUID
        # 因此用 fdisk

        # Disk identifier: 0x36778223                                  # gnu fdisk + mbr
        # Disk identifier: D6B17C1A-FA1E-40A1-BDCB-0278A3ED9CFC        # gnu fdisk + gpt
        # Disk identifier (GUID): d6b17c1a-fa1e-40a1-bdcb-0278a3ed9cfc # busybox fdisk + gpt
        # 不显示 Disk identifier                                        # busybox fdisk + mbr

        # 获取 xda 的 id
        install_pkg fdisk
        main_disk=$(fdisk -l /dev/$xda | grep 'Disk identifier' | awk '{print $NF}' | sed 's/0x//')
    fi

    # 检查 id 格式是否正确
    if ! grep -Eix '[0-9a-f]{8}' <<<"$main_disk" &&
        ! grep -Eix '[0-9a-f-]{36}' <<<"$main_disk"; then
        error_and_exit "Disk ID is invalid: $main_disk"
    fi
}

is_found_ipv4_netconf() {
    [ -n "$ipv4_mac" ] && [ -n "$ipv4_addr" ] && [ -n "$ipv4_gateway" ]
}

is_found_ipv6_netconf() {
    [ -n "$ipv6_mac" ] && [ -n "$ipv6_addr" ] && [ -n "$ipv6_gateway" ]
}

# TODO: 单网卡多IP
collect_netconf() {
    if is_in_windows; then
        convert_net_str_to_array() {
            config=$1
            key=$2
            var=$3
            IFS=',' read -r -a "${var?}" <<<"$(grep "$key=" <<<"$config" | cut -d= -f2 | sed 's/[{}\"]//g')"
        }

        # 部分机器精简了 powershell
        # 所以不要用 powershell 获取网络信息
        # ids=$(wmic nic where "PhysicalAdapter=true and MACAddress is not null and (PNPDeviceID like '%VEN_%&DEV_%' or PNPDeviceID like '%{F8615163-DF3E-46C5-913F-F2D2F965ED0E}%')" get InterfaceIndex | sed '1d')

        # 否        手动        0    0.0.0.0/0                  19  192.168.1.1
        # 否        手动        0    0.0.0.0/0                  59  nekoray-tun

        # wmic nic:
        # 真实网卡
        # AdapterType=以太网 802.3
        # AdapterTypeId=0
        # MACAddress=68:EC:C5:11:11:11
        # PhysicalAdapter=TRUE
        # PNPDeviceID=PCI\VEN_8086&DEV_095A&SUBSYS_94108086&REV_61\4&295A4BD&1&00E0

        # VPN tun 网卡,部分移动云电脑也有
        # AdapterType=
        # AdapterTypeId=
        # MACAddress=
        # PhysicalAdapter=TRUE
        # PNPDeviceID=SWD\WINTUN\{6A460D48-FB76-6C3F-A47D-EF97D3DC6B0E}

        # VMware 网卡
        # AdapterType=以太网 802.3
        # AdapterTypeId=0
        # MACAddress=00:50:56:C0:00:08
        # PhysicalAdapter=TRUE
        # PNPDeviceID=ROOT\VMWARE\0001

        for v in 4 6; do
            if [ "$v" = 4 ]; then
                # 或者 route print
                routes=$(netsh int ipv4 show route | awk '$4 == "0.0.0.0/0"')
            else
                routes=$(netsh int ipv6 show route | awk '$4 == "::/0"')
            fi

            if [ -z "$routes" ]; then
                continue
            fi

            while read -r route; do
                if false; then
                    read -r _ _ _ _ id gateway <<<"$route"
                else
                    id=$(awk '{print $5}' <<<"$route")
                    gateway=$(awk '{print $6}' <<<"$route")
                fi

                config=$(wmic nicconfig where InterfaceIndex=$id get MACAddress,IPAddress,IPSubnet,DefaultIPGateway)
                # 排除 IP/子网/网关/MAC 为空的
                if grep -q '=$' <<<"$config"; then
                    continue
                fi

                mac_addr=$(grep "MACAddress=" <<<"$config" | cut -d= -f2 | to_lower)
                convert_net_str_to_array "$config" IPAddress ips
                convert_net_str_to_array "$config" IPSubnet subnets
                convert_net_str_to_array "$config" DefaultIPGateway gateways

                # IPv4
                # shellcheck disable=SC2154
                if [ "$v" = 4 ]; then
                    for ((i = 0; i < ${#ips[@]}; i++)); do
                        ip=${ips[i]}
                        subnet=${subnets[i]}
                        if [[ "$ip" = *.* ]]; then
                            # ipcalc 依赖 perl,会使 cygwin 增加 ~50M
                            # cidr=$(ipcalc -b "$ip/$subnet" | grep Netmask: | awk '{print $NF}')
                            cidr=$(mask2cidr "$subnet")
                            ipv4_addr="$ip/$cidr"
                            ipv4_gateway="$gateway"
                            ipv4_mac="$mac_addr"
                            # 只取第一个 IP
                            break
                        fi
                    done
                fi

                # IPv6
                if [ "$v" = 6 ]; then
                    ipv6_type_list=$(netsh interface ipv6 show address $id normal)
                    for ((i = 0; i < ${#ips[@]}; i++)); do
                        ip=${ips[i]}
                        cidr=${subnets[i]}
                        if [[ "$ip" = *:* ]]; then
                            ipv6_type=$(grep "$ip" <<<"$ipv6_type_list" | awk '{print $1}')
                            # Public 是 slaac
                            # 还有类型 Temporary,不过有 Temporary 肯定还有 Public,因此不用
                            if [ "$ipv6_type" = Public ] ||
                                [ "$ipv6_type" = Dhcp ] ||
                                [ "$ipv6_type" = Manual ]; then
                                ipv6_addr="$ip/$cidr"
                                ipv6_gateway="$gateway"
                                ipv6_mac="$mac_addr"
                                # 只取第一个 IP
                                break
                            fi
                        fi
                    done
                fi

                # 网关
                # shellcheck disable=SC2154
                if false; then
                    for gateway in "${gateways[@]}"; do
                        if [ -n "$ipv4_addr" ] && [[ "$gateway" = *.* ]]; then
                            ipv4_gateway="$gateway"
                        elif [ -n "$ipv6_addr" ] && [[ "$gateway" = *:* ]]; then
                            ipv6_gateway="$gateway"
                        fi
                    done
                fi

                # 如果通过本条 route 的网卡找到了 IP 则退出 routes 循环
                if is_found_ipv${v}_netconf; then
                    break
                fi
            done < <(echo "$routes")
        done
    else
        # linux
        # 通过默认网关得到默认网卡

        # 多个默认路由下
        # ip -6 route show default dev ens3 完全不显示

        # ip -6 route show default
        # default proto static metric 1024 pref medium
        #         nexthop via 2a01:1111:262:4940::2 dev ens3 weight 1 onlink
        #         nexthop via fe80::5054:ff:fed4:5286 dev ens3 weight 1

        # ip -6 route show default
        # default via 2602:1111:0:80::1 dev eth0 metric 1024 onlink pref medium

        # arch + vultr
        # ip -6 route show default
        # default nhid 4011550343 via fe80::fc00:5ff:fe3d:2714 dev enp1s0 proto ra metric 1024 expires 1504sec pref medium

        for v in 4 6; do
            if via_gateway_dev_ethx=$(ip -$v route show default | grep -Ewo 'via [^ ]+ dev [^ ]+' | head -1 | grep .); then
                read -r _ gateway _ ethx <<<"$via_gateway_dev_ethx"
                eval ipv${v}_ethx="$ethx" # can_use_cloud_kernel 要用
                eval ipv${v}_mac="$(ip link show dev $ethx | grep link/ether | head -1 | awk '{print $2}')"
                eval ipv${v}_gateway="$gateway"

                # 获取所有全局地址
                all_addrs=$(ip -$v -o addr show scope global dev $ethx | grep -v temporary | awk '{print $4}')
                primary_addr=$(echo "$all_addrs" | head -1)

                # IPv6: 用 ip route get 让内核返回正确的源 IP,指定 dev 避免 tun/warp 干扰
                if [ "$v" = 6 ] && [ -n "$primary_addr" ]; then
                    route_src=$(ip -6 route get 2001:4860:4860::8888 dev "$ethx" 2>/dev/null | grep -oP 'src \K[^ ]+')
                    if [ -n "$route_src" ]; then
                        for addr in $all_addrs; do
                            if [ "${addr%/*}" = "$route_src" ]; then
                                primary_addr=$addr
                                break
                            fi
                        done
                    fi
                fi

                eval ipv${v}_addr="$primary_addr"
                # extra_addrs: 除主地址外的所有地址
                eval ipv${v}_extra_addrs="$(echo "$all_addrs" | grep -Fxve "$primary_addr" | tr '\n' ',' | sed 's/,$//')"
            fi
        done
    fi

    if ! is_found_ipv4_netconf && ! is_found_ipv6_netconf; then
        error_and_exit "Can not get IP info."
    fi

    info "Network Info"
    echo "IPv4 MAC: $ipv4_mac"
    echo "IPv4 Address: $ipv4_addr"
    echo "IPv4 Gateway: $ipv4_gateway"
    echo "---"
    echo "IPv6 MAC: $ipv6_mac"
    echo "IPv6 Address: $ipv6_addr"
    echo "IPv6 Gateway: $ipv6_gateway"
    echo
}

get_efi_dir_in_windows() {
    # 挂载
    if result=$(find /cygdrive/?/EFI/Microsoft/Boot/bootmgfw.efi 2>/dev/null); then
        # 已经挂载
        x=$(echo $result | cut -d/ -f3)
    else
        # 找到空盘符并挂载
        for x in {a..z}; do
            [ ! -e /cygdrive/$x ] && break
        done
        if ! mountvol $x: /s >&2; then
            error_and_exit "Can't mount efi partition in windows."
        fi
    fi
    echo "/cygdrive/$x"
}

add_efi_entry_in_windows() {
    info "Add efi entry in windows"

    local source=$1

    # 文件夹命名为reinstall而不是grub,因为可能机器已经安装了grub,bcdedit名字同理
    dist_dir="$(get_efi_dir_in_windows)/EFI/reinstall"
    efi_drive=$(echo "$dist_dir" | cut -d/ -f3)
    basename=$(basename $source)
    download_or_copy_file "$source" "$dist_dir/$basename"

    # 如果 {fwbootmgr} displayorder 为空
    # 执行 bcdedit /copy '{bootmgr}' 会报错
    # 例如 azure windows 2016 模板
    # 要先设置默认的 {fwbootmgr} displayorder
    # https://github.com/hakuna-m/wubiuefi/issues/286
    bcdedit /set '{fwbootmgr}' displayorder '{bootmgr}' /addfirst

    # 添加启动项
    id=$(bcdedit /copy '{bootmgr}' /d "$(get_entry_name)" | grep -o '{.*}')
    bcdedit /set $id device partition=$efi_drive:
    bcdedit /set $id path \\EFI\\reinstall\\$basename
    bcdedit /set '{fwbootmgr}' bootsequence $id
}

get_maybe_efi_dirs_in_linux() {
    # 不从 fstab 查找,因为极端情况下可能只用 systemd mount
    # arch云镜像efi分区挂载在/efi,且使用 autofs,mount 命令会有两个 /efi 条目

    install_pkg findmnt >&2

    # 寻找 efi 分区,并输出根目录
    # root_dirs=$(mount | awk '$5=="vfat" || $5=="autofs" {print $3}' | grep -Ex '/efi|/boot/efi|/boot' | sort -u)
    root_dirs=$(findmnt -t fat,vfat -n -o TARGET | grep -Ex '/efi|/boot/efi|/boot' | sort -u)

    efi_dirs=$(
        for dir in $root_dirs; do
            # 只显示有 efi 文件的
            # -quit 表示找到第一个 *.efi 就立即退出 find
            if [ -d "$dir" ]; then
                find "$dir" -type f -iname "*.efi" -exec printf '%s\n' "$dir" \; -quit
            fi
        done
    )

    if [ -z "$efi_dirs" ]; then
        error_and_exit "Can't find efi partition."
    fi

    echo "$efi_dirs"
}

get_disk_by_part() {
    dev_part=$1
    install_pkg lsblk >&2
    lsblk -rn --inverse "$dev_part" | grep -w disk | awk '{print $1}'
}

get_part_num_by_part() {
    dev_part=$1
    grep -oE '[0-9]*$' <<<"$dev_part"
}

grep_efi_entry() {
    # efibootmgr
    # BootCurrent: 0002
    # Timeout: 1 seconds
    # BootOrder: 0000,0002,0003,0001
    # Boot0000* sles-secureboot
    # Boot0001* CD/DVD Rom
    # Boot0002* Hard Disk
    # Boot0003* sles-secureboot
    # MirroredPercentageAbove4G: 0.00
    # MirrorMemoryBelow4GB: false

    # 根据文档,* 表示 active,也就是说有可能没有*(代表inactive)
    # https://manpages.debian.org/testing/efibootmgr/efibootmgr.8.en.html
    grep -E '^Boot[0-9a-fA-F]{4}'
}

# trans.sh 有同名方法
grep_efi_index() {
    awk '{print $1}' | sed -e 's/Boot//' -e 's/\*//'
}

download_or_copy_file() {
    local source=$1
    local dist=$2

    mkdir -p "$(dirname $dist)"

    if [[ "$source" = http* ]]; then
        curl -Lo "$dist" "$source"
    else
        cp -f "$source" "$dist"
    fi
}

add_efi_entry_in_linux() {
    local source=$1

    info "Add efi entry in linux"

    install_pkg efibootmgr

    # 只取第一个
    # 因为不用关心是否为 efi 分区,只要是 fat/vfat 格式的分区即可添加到引导
    # 这里用了管道导致 get_maybe_efi_dirs_in_linux 里面的 error_and_exit 不生效
    efi_part=$(get_maybe_efi_dirs_in_linux | head -1 | grep .)
    dist_dir=$efi_part/EFI/reinstall
    basename=$(basename $source)
    download_or_copy_file "$source" "$dist_dir/$basename"

    # 原系统可能不是用 grub 引导,因此不一定有 grub-probe
    if false; then
        grub_probe="$(command -v grub-probe grub2-probe | head -1)"
        dev_part="$("$grub_probe" -t device "$dist_dir")"
    else
        install_pkg findmnt
        # arch findmnt 会得到
        # systemd-1
        # /dev/sda2
        dev_part=$(findmnt -T "$dist_dir" -no SOURCE | grep '^/dev/')
    fi

    if ! {
        res=$(efibootmgr --create-only \
            --disk "/dev/$(get_disk_by_part $dev_part)" \
            --part "$(get_part_num_by_part $dev_part)" \
            --label "$(get_entry_name)" \
            --loader "\\EFI\\reinstall\\$basename") &&
            id=$(echo "$res" | grep_efi_entry | tail -1 | grep_efi_index | grep .) &&
            efibootmgr --bootnext "$id"
    }; then
        echo "$res"
        error_and_exit "Could not add efi entry."
    fi
}

get_grub_efi_filename() {
    case "$basearch" in
    x86_64) echo grubx64.efi ;;
    aarch64) echo grubaa64.efi ;;
    esac
}

install_grub_linux_efi() {
    info 'download grub efi'

    # fedora 39 的 efi 无法识别 opensuse tumbleweed 的 xfs
    efi_distro=fedora

    grub_efi=$(get_grub_efi_filename)

    # 不要用 download.opensuse.org 和 download.fedoraproject.org
    # 因为 ipv6 访问有时跳转到 ipv4 地址,造成 ipv6 only 机器无法下载
    # 日韩机器有时得到国内镜像源,但镜像源屏蔽了国外 IP 导致连不上
    # https://mirrors.bfsu.edu.cn/opensuse/ports/aarch64/tumbleweed/repo/oss/EFI/BOOT/grub.efi

    # fcix 经常 404
    # https://mirror.fcix.net/opensuse/tumbleweed/repo/oss/EFI/BOOT/bootx64.efi
    # https://mirror.fcix.net/opensuse/tumbleweed/appliances/openSUSE-Tumbleweed-Minimal-VM.x86_64-Cloud.qcow2

    # dl.fedoraproject.org 不支持 ipv6

    if [ "$efi_distro" = fedora ]; then
        # fedora 43 efi 在 vultr 无法引导 debain 9/10 netboot
        fedora_ver=$(get_latest_distro_releasever fedora)

        if is_in_china; then
            mirror=https://mirror.nju.edu.cn/fedora
        else
            mirror=https://d2lzkl7pfhq30w.cloudfront.net/pub/fedora/linux
        fi

        curl -Lo $tmp/$grub_efi $mirror/releases/$fedora_ver/Everything/$basearch/os/EFI/BOOT/$grub_efi
    else
        if is_in_china; then
            mirror=https://mirror.nju.edu.cn/opensuse
        else
            mirror=https://downloadcontentcdn.opensuse.org
        fi

        [ "$basearch" = x86_64 ] && ports='' || ports=/ports/$basearch

        curl -Lo $tmp/$grub_efi $mirror$ports/tumbleweed/repo/oss/EFI/BOOT/grub.efi
    fi

    add_efi_entry_in_linux $tmp/$grub_efi
}

download_and_extract_apk() {
    local alpine_ver=$1
    local package=$2
    local extract_dir=$3

    install_pkg tar xz
    is_in_china && mirror=http://mirror.nju.edu.cn/alpine || mirror=https://dl-cdn.alpinelinux.org/alpine
    package_apk=$(curl -L $mirror/v$alpine_ver/main/$basearch/ | grep -oP "$package-[^-]*-[^-]*\.apk" | sort -u)
    if ! [ "$(wc -l <<<"$package_apk")" -eq 1 ]; then
        error_and_exit "find no/multi apks."
    fi
    mkdir -p "$extract_dir"

    # 屏蔽警告
    tar 2>&1 | grep -q BusyBox && tar_args= || tar_args=--warning=no-unknown-keyword
    curl -L "$mirror/v$alpine_ver/main/$basearch/$package_apk" | tar xz $tar_args -C "$extract_dir"
}

install_grub_win() {
    # 下载 grub
    info download grub

    # arm64 模块要单独下载,要注意版本匹配
    grub_ver=2.06

    # ftpmirror.gnu.org 是 geoip 重定向,不是 cdn
    # 有可能重定义到一个拉黑了部分 IP 的服务器

    # 换成 ftp.gnu.org?
    is_in_china && grub_url=https://mirror.nju.edu.cn/gnu/grub/grub-$grub_ver-for-windows.zip ||
        grub_url=https://mirrors.kernel.org/gnu/grub/grub-$grub_ver-for-windows.zip
    curl -Lo $tmp/grub.zip $grub_url
    # unzip -qo $tmp/grub.zip
    7z x $tmp/grub.zip -o$tmp -r -y -xr!i386-efi -xr!locale -xr!themes -bso0
    grub_dir=$tmp/grub-$grub_ver-for-windows
    grub=$grub_dir/grub

    # 设置 grub 包含的模块
    # 原系统是 windows,因此不需要 ext2 lvm xfs btrfs
    grub_modules+=" normal minicmd serial ls echo test cat reboot halt linux chain search all_video configfile"
    grub_modules+=" scsi part_msdos part_gpt fat ntfs ntfscomp lzopio xzio gzio zstd"
    if ! is_efi; then
        grub_modules+=" biosdisk linux16"
    fi

    # 设置 grub prefix 为c盘根目录
    # 运行 grub-probe 会改变cmd窗口字体
    prefix=$($grub-probe -t drive $c: | sed 's|.*PhysicalDrive|(hd|' | del_cr)/
    echo $prefix

    # 安装 grub
    if is_efi; then
        # efi
        info install grub for efi

        case "$basearch" in
        x86_64) grub_arch=x86_64 ;;
        aarch64) grub_arch=arm64 ;;
        esac

        # 下载 grub arm64 模块
        # 注意要匹配 grub-for-windows 版本
        if ! [ -d $grub_dir/grub/$grub_arch-efi ]; then
            # 3.20 是 grub 2.12,可能会有问题
            alpine_ver=3.19
            download_and_extract_apk $alpine_ver grub-efi $tmp/grub-efi
            cp -r $tmp/grub-efi/usr/lib/grub/$grub_arch-efi/ $grub_dir
        fi

        grub_efi=$(get_grub_efi_filename)
        $grub-mkimage -p $prefix -O $grub_arch-efi -o "$(cygpath -w "$grub_dir/$grub_efi")" $grub_modules
        add_efi_entry_in_windows "$grub_dir/$grub_efi"
    else
        # bios
        info install grub for bios

        # bootmgr 加载 g2ldr 有大小限制
        # 超过大小会报错 0xc000007b
        # 解决方法1 g2ldr.mbr + g2ldr
        # 解决方法2 生成少于64K的 g2ldr + 动态模块
        if false; then
            # g2ldr.mbr
            # 部分国内机无法访问 ftp.cn.debian.org
            is_in_china && host=mirror.nju.edu.cn || host=deb.debian.org
            curl -LO http://$host/debian/tools/win32-loader/oldstable/win32-loader.exe
            7z x win32-loader.exe 'g2ldr.mbr' -o$tmp/win32-loader -r -y -bso0
            find $tmp/win32-loader -name 'g2ldr.mbr' -exec cp {} /cygdrive/$c/ \;

            # g2ldr
            # 配置文件 c:\grub.cfg
            $grub-mkimage -p "$prefix" -O i386-pc -o "$(cygpath -w $grub_dir/core.img)" $grub_modules
            cat $grub_dir/i386-pc/lnxboot.img $grub_dir/core.img >/cygdrive/$c/g2ldr
        else
            # grub-install 无法设置 prefix
            # 配置文件 c:\grub\grub.cfg
            $grub-install $c \
                --target=i386-pc \
                --boot-directory=$c: \
                --install-modules="$grub_modules" \
                --themes= \
                --fonts= \
                --no-bootsector

            cat $grub_dir/i386-pc/lnxboot.img /cygdrive/$c/grub/i386-pc/core.img >/cygdrive/$c/g2ldr
        fi

        # 添加引导
        # 脚本可能不是首次运行,所以先删除原来的
        id='{1c41f649-1637-52f1-aea8-f96bfebeecc8}'
        bcdedit /enum all | grep -a $id && bcdedit /delete $id
        bcdedit /create $id /d "$(get_entry_name)" /application bootsector
        bcdedit /set $id device partition=$c:
        bcdedit /set $id path \\g2ldr
        bcdedit /displayorder $id /addlast
        bcdedit /bootsequence $id /addfirst
    fi
}

find_grub_extlinux_cfg() {
    dir=$1
    filename=$2
    keyword=$3

    # 当 ln -s /boot/grub /boot/grub2 时
    # find /boot/ 会自动忽略 /boot/grub2 里面的文件
    cfgs=$(
        # 只要 $dir 存在
        # 无论是否找到结果,返回值都是 0
        find $dir \
            -type f -name $filename \
            -exec grep -E -l "$keyword" {} \;
    )

    count="$(wc -l <<<"$cfgs")"
    if [ "$count" -eq 1 ]; then
        echo "$cfgs"
    else
        error_and_exit "Find $count $filename."
    fi
}

# 空格、&、用户输入的网址要加引号,否则 grub 无法正确识别
is_need_quote() {
    [[ "$1" = *' '* ]] || [[ "$1" = *'&'* ]] || [[ "$1" = http* ]]
}

# 转换 finalos_a=1 为 finalos.a=1 ,排除 finalos_mirrorlist
build_finalos_cmdline() {
    if vars=$(compgen -v finalos_); then
        for key in $vars; do
            value=${!key}
            key=${key#finalos_}
            if [ -n "$value" ] && [ $key != "mirrorlist" ]; then
                is_need_quote "$value" &&
                    finalos_cmdline+=" finalos_$key='$value'" ||
                    finalos_cmdline+=" finalos_$key=$value"
            fi
        done
    fi
}

build_extra_cmdline() {
    # 使用 extra_xxx=yyy 而不是 extra.xxx=yyy
    # 因为 debian installer /lib/debian-installer-startup.d/S02module-params
    # 会将 extra.xxx=yyy 写入新系统的 /etc/modprobe.d/local.conf
    # https://answers.launchpad.net/ubuntu/+question/249456
    # https://salsa.debian.org/installer-team/rootskel/-/blob/master/src/lib/debian-installer-startup.d/S02module-params?ref_type=heads
    for key in confhome hold force_boot_mode force_cn force_old_windows_setup cloud_image main_disk \
        elts deb_mirror \
        ssh_port rdp_port web_port allow_ping; do
        value=${!key}
        if [ -n "$value" ]; then
            is_need_quote "$value" &&
                extra_cmdline+=" extra_$key='$value'" ||
                extra_cmdline+=" extra_$key=$value"
        fi
    done

    # 指定最终安装系统的 mirrorlist,链接有&,在grub中是特殊字符,所以要加引号
    if [ -n "$finalos_mirrorlist" ]; then
        extra_cmdline+=" extra_mirrorlist='$finalos_mirrorlist'"
    elif [ -n "$nextos_mirrorlist" ]; then
        extra_cmdline+=" extra_mirrorlist='$nextos_mirrorlist'"
    fi

    # cloudcone 特殊处理
    if is_grub_dir_linked; then
        finalos_cmdline+=" extra_link_grub_dir=1"
    fi
}

echo_tmp_ttys() {
    if false; then
        curl -L $confhome/ttys.sh | sh -s "console="
    else
        case "$basearch" in
        x86_64) echo "console=ttyS0,115200n8 console=tty0" ;;
        aarch64) echo "console=ttyS0,115200n8 console=ttyAMA0,115200n8 console=tty0" ;;
        esac
    fi
}

get_entry_name() {
    printf 'reinstall ('
    printf '%s' "$distro"
    [ -n "$releasever" ] && printf ' %s' "$releasever"
    [ "$distro" = alpine ] && [ "$hold" = 1 ] && printf ' Live OS'
    printf ')'
}

# shellcheck disable=SC2154
build_nextos_cmdline() {
    if [ $nextos_distro = alpine ]; then
        nextos_cmdline="alpine_repo=$nextos_repo modloop=$nextos_modloop"
    elif is_distro_like_debian $nextos_distro; then
        # 设置分辨率为800*600,防止分辨率过高 ssh screen attach 后无法全部显示
        # iso 默认有 vga=788
        # 如果要设置位数: video=800x600-16
        nextos_cmdline="lowmem/low=1 auto=true priority=critical"
        # nextos_cmdline+=" vga=788 video=800x600"
        nextos_cmdline+=" url=$nextos_ks"
        nextos_cmdline+=" mirror/http/hostname=${nextos_udeb_mirror%/*}"
        nextos_cmdline+=" mirror/http/directory=/${nextos_udeb_mirror##*/}"
        nextos_cmdline+=" base-installer/kernel/image=$nextos_kernel"
        # elts 的 debian 不能用 security 源,否则安装过程会提示无法访问
        if [ "$nextos_distro" = debian ] && is_debian_elts; then
            nextos_cmdline+=" apt-setup/services-select="
        fi
        # kali 安装好后网卡是 eth0 这种格式,但安装时不是
        if [ "$nextos_distro" = kali ]; then
            nextos_cmdline+=" net.ifnames=0"
            nextos_cmdline+=" simple-cdd/profiles=kali"
        fi
    elif is_distro_like_redhat $nextos_distro; then
        # redhat
        nextos_cmdline="root=live:$nextos_squashfs inst.ks=$nextos_ks"
    fi

    if is_distro_like_debian $nextos_distro; then
        if [ "$basearch" = "x86_64" ]; then
            # debian installer 好像第一个 tty 是主 tty
            # 设置ttyS0,tty0,安装界面还是显示在ttyS0
            :
        else
            # debian arm 在没有ttyAMA0的机器上(aws t4g),最少要设置一个tty才能启动
            # 只设置tty0也行,但安装过程ttyS0没有显示
            nextos_cmdline+=" $(echo_tmp_ttys)"
        fi
    else
        nextos_cmdline+=" $(echo_tmp_ttys)"
    fi
    # nextos_cmdline+=" mem=256M"
    # nextos_cmdline+=" lowmem=+1"
}

build_cmdline() {
    # nextos
    build_nextos_cmdline

    # finalos
    # trans 需要 finalos_distro 识别是安装 alpine 还是其他系统
    if [ "$distro" = alpine ]; then
        finalos_distro=alpine
    fi
    if [ -n "$finalos_distro" ]; then
        build_finalos_cmdline
    fi

    # extra
    build_extra_cmdline

    cmdline="$nextos_cmdline $finalos_cmdline $extra_cmdline"
}

# 脚本可能多次运行,先清理之前的残留
mkdir_clear() {
    dir=$1

    if [ -z "$dir" ] || [ "$dir" = / ]; then
        return
    fi

    # 再次运行时,有可能 mount 了 btrfs root,因此先要 umount_all
    # 但目前不需要 mount ,因此用不到
    # umount_all "$dir"
    rm -rf "$dir"
    mkdir -p "$dir"
}

mod_initrd_debian_kali() {
    # hack 1
    # 允许设置 ipv4 onlink 网关
    sed -Ei 's,&&( onlink=),||\1,' etc/udhcpc/default.script

    # hack 2
    # 强制使用 screen
    # shellcheck disable=SC1003,SC2016
    {
        echo 'if false && : \' | insert_into_file lib/debian-installer.d/S70menu before 'if [ -x "$bterm" ]' -F
        echo 'if true  || : \' | insert_into_file lib/debian-installer.d/S70menu before 'if [ -x "$screen_bin" -a' -F
    }

    # hack 3
    # 修改 /var/lib/dpkg/info/netcfg.postinst 运行我们的脚本
    netcfg() {
        #!/bin/sh
        # shellcheck source=/dev/null
        . /usr/share/debconf/confmodule
        db_progress START 0 5 debian-installer/netcfg/title

        : get_ip_conf_cmd

        # 运行 trans.sh,保存配置
        db_progress INFO base-installer/progress/netcfg
        # 添加 || exit ,可以在 debian installer 不兼容 /trans.sh 语法时强制报错
        # exit 不带参数,返回值为 || 前面命令的返回值
        sh /trans.sh || exit
        db_progress STEP 1
        db_progress STOP
    }

    postinst=var/lib/dpkg/info/netcfg.postinst
    get_function_content netcfg >$postinst
    get_ip_conf_cmd | insert_into_file $postinst after ": get_ip_conf_cmd"
    # cat $postinst

    # hack 4
    # 修改 udeb 依赖

    # 直接覆盖 net-retriever,方便调试
    # curl -Lo /usr/lib/debian-installer/retriever/net-retriever $confhome/net-retriever

    change_priority() {
        while IFS= read -r line; do
            if [[ "$line" = Package:* ]]; then
                package=$(echo "$line" | cut -d' ' -f2-)

            elif [[ "$line" = Priority:* ]]; then
                # shellcheck disable=SC2154
                if [ "$line" = "Priority: standard" ]; then
                    for p in $disabled_list; do
                        if [ "$package" = "$p" ]; then
                            line="Priority: optional"
                            break
                        fi
                    done
                elif [[ "$package" = ata-modules* ]]; then
                    # 改成强制安装
                    # 因为是 pata-modules sata-modules scsi-modules 的依赖
                    # 但我们没安装它们,也就不会自动安装 ata-modules
                    line="Priority: standard"
                fi
            fi
            echo "$line"
        done
    }

    # shellcheck disable=SC2012
    kver=$(ls -d lib/modules/* | awk -F/ '{print $NF}')

    net_retriever=usr/lib/debian-installer/retriever/net-retriever
    # shellcheck disable=SC2016
    sed -i 's,>> "$1",| change_priority >> "$1",' $net_retriever
    insert_into_file $net_retriever after '#!/bin/sh' <$deb_list
        fi

        # 下载 deb/udeb
        deb_path=$(grep -F "/${package}_" "$deb_list")
        curl -Lo $tmp/tmp.deb http://$mirror/"$deb_path"

        if false; then
            # 使用 dpkg
            # cygwin 没有 dpkg
            install_pkg dpkg
            dpkg -x $tmp/tmp.deb $extract_dir
        else
            # 使用 ar tar xz
            # cygwin 需安装 binutils
            # centos7 ar 不支持 --output
            install_pkg ar tar xz
            (cd $tmp && ar x $tmp/tmp.deb)
            tar xf $tmp/data.tar.xz -C $extract_dir
        fi
    }

    # 不用在 windows 判断是哪种硬盘控制器,因为 256M 运行 windows 只可能是 xp,而脚本本来就不支持 xp
    # 在 debian installer 中判断能否用云内核
    create_can_use_cloud_kernel_sh can_use_cloud_kernel.sh

    # 下载 fix-eth-name 脚本
    curl -LO "$confhome/fix-eth-name.sh"
    curl -LO "$confhome/fix-eth-name.service"

    # 有段时间 kali initrd 删除了原版 wget
    # 但 initrd 的 busybox wget 又不支持 https
    # 因此改成在这里下载
    curl -LO "$confhome/get-xda.sh"
    curl -LO "$confhome/ttys.sh"
    if [ -n "$frpc_config" ]; then
        curl -LO "$confhome/get-frpc-url.sh"
        curl -LO "$confhome/frpc.service"
    fi

    # 可以节省一点内存?
    echo 'export DEBCONF_DROP_TRANSLATIONS=1' |
        insert_into_file lib/debian-installer/menu before 'exec debconf'

    # 还原 kali netinst.iso 的 simple-cdd 机制
    # 主要用于调用 kali.postinst 设置 zsh 为默认 shell
    # 但 mini.iso 又没有这种机制
    # https://gitlab.com/kalilinux/build-scripts/kali-live/-/raw/main/kali-config/common/includes.installer/kali-finish-install?ref_type=heads
    # https://salsa.debian.org/debian/simple-cdd/-/blob/master/debian/14simple-cdd?ref_type=heads
    # https://http.kali.org/pool/main/s/simple-cdd/simple-cdd-profiles_0.6.9_all.udeb
    if [ "$distro" = kali ]; then
        # 但我们没有使用 iso,因此没有 kali.postinst,需要另外下载
        mkdir -p cdrom/simple-cdd
        curl -Lo cdrom/simple-cdd/kali.postinst https://gitlab.com/kalilinux/build-scripts/kali-live/-/raw/main/kali-config/common/includes.installer/kali-finish-install?ref_type=heads
        chmod a+x cdrom/simple-cdd/kali.postinst
    fi

    if [ "$distro" = debian ] && is_debian_elts; then
        curl -Lo usr/share/keyrings/debian-archive-keyring.gpg https://deb.freexian.com/extended-lts/archive-key.gpg
    fi

    # 提前下载 sshd
    # 以便在配置下载源之前就可以启动 sshd
    mkdir_clear $tmp/sshd
    download_and_extract_deb udeb openssh-server-udeb $tmp/sshd
    cp -r $tmp/sshd/* .

    # 提前下载 fdisk
    # 因为 fdisk-udeb 包含 fdisk 和 sfdisk,提前下载可减少占用
    mkdir_clear $tmp/fdisk
    download_and_extract_deb udeb fdisk-udeb $tmp/fdisk
    cp -f $tmp/fdisk/usr/sbin/fdisk usr/sbin/

    # 下载 websocketd
    # debian 11+ 才有 websocketd
    if [ "$distro" = kali ] ||
        { [ "$distro" = debian ] && [ "$releasever" -ge 11 ]; }; then
        mkdir_clear $tmp/websocketd
        download_and_extract_deb deb websocketd $tmp/websocketd
        cp -f $tmp/websocketd/usr/bin/websocketd usr/bin/
    fi

    # >256M 或者当前系统是 windows
    if [ $ram_size -gt 256 ] || is_in_windows; then
        sed -i '/^pata-modules/d' $net_retriever
        sed -i '/^sata-modules/d' $net_retriever
        sed -i '/^scsi-modules/d' $net_retriever
    else
        # <=256M 极限优化
        find_main_disk
        extra_drivers=
        for driver in $(get_disk_drivers $xda); do
            echo "using driver: $driver"
            case $driver in
            nvme) extra_drivers+=" nvme nvme-core" ;;
                # xen 的横杠特别不同
            xen_blkfront) extra_drivers+=" xen-blkfront" ;;
            xen_scsifront) extra_drivers+=" xen-scsifront" ;;
            virtio_blk | virtio_scsi | hv_storvsc | vmw_pvscsi) extra_drivers+=" $driver" ;;
            pata_legacy) sed -i '/^pata-modules/d' $net_retriever ;; # 属于 pata-modules
            ata_piix) sed -i '/^sata-modules/d' $net_retriever ;;    # 属于 sata-modules
            ata_generic) ;;                                          # 属于 ata-modules,不用处理,因为我们设置强制安装了 ata-modules
            esac
        done

        # extra drivers
        # xen 还需要以下两个?
        # kernel/drivers/xen/xen-scsiback.ko
        # kernel/drivers/block/xen-blkback/xen-blkback.ko
        # 但反查也找不到 curl https://deb.debian.org/debian/dists/bookworm/main/Contents-udeb-amd64.gz | zcat | grep xen
        if [ -n "$extra_drivers" ]; then
            mkdir_clear $tmp/scsi
            download_and_extract_deb udeb scsi-modules-$kver-di $tmp/scsi
            relative_drivers_dir=lib/modules/$kver/kernel/drivers

            udeb_drivers_dir=$tmp/scsi/$relative_drivers_dir
            dist_drivers_dir=$initrd_dir/$relative_drivers_dir
            (
                cd $udeb_drivers_dir
                for driver in $extra_drivers; do
                    # debian 模块没有压缩
                    # kali 模块有压缩
                    # 因此要有 *
                    if ! find $dist_drivers_dir -name "$driver.ko*" | grep -q .; then
                        echo "adding driver: $driver"
                        file=$(find . -name "$driver.ko*" | grep .)
                        cp -fv --parents "$file" "$dist_drivers_dir"
                    fi
                done
            )
        fi
    fi

    # amd64)
    # 	level1=737 # MT=754108, qemu: -m 780
    # 	level2=424 # MT=433340, qemu: -m 460
    # 	min=316    # MT=322748, qemu: -m 350

    # 将 use_level 2 9 修改为 use_level 1
    # x86 use_level 2 会出现 No root file system is defined.
    # arm 即使 use_level 1 也会出现 No root file system is defined.
    sed -i 's/use_level=[29]/use_level=1/' lib/debian-installer-startup.d/S15lowmem

    # hack 3
    # 修改 trans.sh
    # 1. 直接调用 create_ifupdown_config
    # shellcheck disable=SC2154
    insert_into_file $initrd_dir/trans.sh after '^: main' < >
    # 5. debian 11/12 initrd 无法识别 < <
    # 6. debian 11 initrd 无法识别 set -E
    # 7. debian 11 initrd 无法识别 trap ERR
    # 8. debian 9 initrd 无法识别 ${string//find/replace}
    # 9. debian 12 initrd 无法识别 . <(
    # 删除或注释,可能会导致空方法而报错,因此改为替换成'\n: #'
    replace='\n: #'
    sed -Ei \
        -e "s/> >/$replace/" \
        -e "s/< $1
        $(get_function get_drivers)
        $(get_function get_net_drivers)
        $(get_function get_disk_drivers)
        $(get_function can_use_cloud_kernel)

        can_use_cloud_kernel "\$@"
EOF
}

get_ip_conf_cmd() {
    collect_netconf >&2
    is_in_china && is_in_china=true || is_in_china=false

    sh=/initrd-network.sh
    if is_found_ipv4_netconf && is_found_ipv6_netconf && [ "$ipv4_mac" = "$ipv6_mac" ]; then
        echo "'$sh' '$ipv4_mac' '$ipv4_addr' '$ipv4_gateway' '$ipv6_addr' '$ipv6_gateway' '$is_in_china' '$ipv6_extra_addrs'"
    else
        if is_found_ipv4_netconf; then
            echo "'$sh' '$ipv4_mac' '$ipv4_addr' '$ipv4_gateway' '' '' '$is_in_china' ''"
        fi
        if is_found_ipv6_netconf; then
            echo "'$sh' '$ipv6_mac' '' '' '$ipv6_addr' '$ipv6_gateway' '$is_in_china' '$ipv6_extra_addrs'"
        fi
    fi
}

mod_initrd_alpine() {
    # hack 1 v3.19 和之前的 virt 内核需添加 ipv6 模块
    if virt_dir=$(ls -d $initrd_dir/lib/modules/*-virt 2>/dev/null); then
        ipv6_dir=$virt_dir/kernel/net/ipv6
        if ! [ -f $ipv6_dir/ipv6.ko ] && ! grep -q ipv6 $initrd_dir/lib/modules/*/modules.builtin; then
            mkdir -p $ipv6_dir
            modloop_file=$tmp/modloop_file
            modloop_dir=$tmp/modloop_dir
            curl -Lo $modloop_file $nextos_modloop
            if is_in_windows; then
                # cygwin 没有 unsquashfs
                7z e $modloop_file ipv6.ko -r -y -o$ipv6_dir
            else
                install_pkg unsquashfs
                mkdir_clear $modloop_dir
                unsquashfs -f -d $modloop_dir $modloop_file 'modules/*/kernel/net/ipv6/ipv6.ko'
                find $modloop_dir -name ipv6.ko -exec cp {} $ipv6_dir/ \;
            fi
        fi
    fi

    # hack 下载 dhcpcd
    # shellcheck disable=SC2154
    download_and_extract_apk "$nextos_releasever" dhcpcd "$initrd_dir"
    sed -i -e '/^slaac private/s/^/#/' -e '/^#slaac hwaddr/s/^#//' $initrd_dir/etc/dhcpcd.conf

    # hack 2 /usr/share/udhcpc/default.script
    # 脚本被调用的顺序
    # udhcpc:  deconfig
    # udhcpc:  bound
    # udhcpc6: deconfig
    # udhcpc6: bound
    # shellcheck disable=SC2329
    udhcpc() {
        if [ "$1" = deconfig ]; then
            return
        fi
        if [ "$1" = bound ] && [ -n "$ipv6" ]; then
            # shellcheck disable=SC2154
            ip -6 addr add "$ipv6" dev "$interface"
            ip link set dev "$interface" up
            return
        fi
    }

    get_function_content udhcpc |
        insert_into_file usr/share/udhcpc/default.script after 'deconfig\|renew\|bound'

    # 允许设置 ipv4 onlink 网关
    sed -Ei 's,(0\.0\.0\.0\/0),"\1 onlink",' usr/share/udhcpc/default.script

    # hack 3 网络配置
    # alpine 根据 MAC_ADDRESS 判断是否有网络
    # https://github.com/alpinelinux/mkinitfs/blob/c4c0115f9aa5aa8884c923dc795b2638711bdf5c/initramfs-init.in#L914
    insert_into_file init after 'configure_ip\(\)' <\$sysroot/etc/local.d/trans.start
        # wget --no-check-certificate -O \$sysroot/etc/local.d/trans.start $confhome/trans.sh
        cp /trans.sh \$sysroot/etc/local.d/trans.start
        chmod a+x \$sysroot/etc/local.d/trans.start
        ln -s /etc/init.d/local \$sysroot/etc/runlevels/default/

        # 配置 + 自定义驱动
        for dir in /configs /custom_drivers; do
            if [ -d \$dir ]; then
                cp -r \$dir \$sysroot/
                rm -rf \$dir
            fi
        done
EOF

    # 判断云镜像 debain 能否用云内核
    if is_distro_like_debian; then
        create_can_use_cloud_kernel_sh can_use_cloud_kernel.sh
        insert_into_file init before '^exec (/bin/busybox )?switch_root' <$initrd_dir/configs/ssh_keys
    else
        save_password $initrd_dir/configs
    fi
    if [ -n "$frpc_config" ]; then
        cat "$frpc_config" >$initrd_dir/configs/frpc.conf
    fi

    # 收集 cloud-data 打包进 initrd
    if [ -n "$cloud_data" ]; then
        mkdir -p $initrd_dir/configs/cloud-data
        if [ -d "$cloud_data" ]; then
            # 本地目录:直接复制
            cp "$cloud_data"/* $initrd_dir/configs/cloud-data/
        else
            # URL:在 host 下载
            for f in user-data meta-data network-config; do
                curl -fsSL "$cloud_data/$f" -o "$initrd_dir/configs/cloud-data/$f" 2>/dev/null || true
            done
        fi
        # 校验:至少要有 user-data
        [ -f $initrd_dir/configs/cloud-data/user-data ] || error_and_exit "--cloud-data must contain user-data"
        cloud_data_files=$(ls $initrd_dir/configs/cloud-data/ | tr '\n' ' ')
    fi

    if is_distro_like_debian $nextos_distro; then
        mod_initrd_debian_kali
    else
        mod_initrd_$nextos_distro
    fi

    # 添加自定义 windows 驱动
    if [ "$distro" = windows ] && [ -n "$custom_infs" ]; then
        # shellcheck disable=SC1090
        . <(curl -L $confhome/windows-driver-utils.sh)
        echo "$custom_infs" | while read -r inf; do
            parse_inf_and_cp_driever "$inf" "$initrd_dir/custom_drivers" "$basearch_alt" true
        done
    fi

    # alpine live 不精简 initrd
    # 因为不知道用户想干什么,可能会用到精简的文件
    if is_virt && ! is_alpine_live; then
        remove_useless_initrd_files
    fi

    if [ "$hold" = 0 ]; then
        info 'hold 0'
        read -r -p 'Press Enter to continue...'
    fi

    # 重建
    # 注意要用 cpio -H newc 不要用 cpio -c ,不同版本的 -c 作用不一样,很坑
    # -c    Use the old portable (ASCII) archive format
    # -c    Identical to "-H newc", use the new (SVR4)
    #       portable format.If you wish the old portable
    #       (ASCII) archive format, use "-H odc" instead.
    find . | cpio --quiet -o -H newc -R 0:0 | gzip -1 >/reinstall-initrd
    cd - >/dev/null
}

remove_useless_initrd_files() {
    info "slim initrd"

    # 显示精简前的大小
    du -sh .

    # 删除 initrd 里面没用的文件/驱动
    rm -rf bin/brltty
    rm -rf etc/brltty
    rm -rf sbin/wpa_supplicant
    rm -rf usr/lib/libasound.so.*
    rm -rf usr/share/alsa
    (
        cd lib/modules/*/kernel/drivers/net/ethernet/
        for item in *; do
            case "$item" in
            # 甲骨文 arm 用自定义镜像支持设为 mlx5 vf 网卡,且不是 azure 那样显示两个网卡
            # https://debian.pkgs.org/13/debian-main-amd64/linux-image-6.12.43+deb13-cloud-amd64_6.12.43-1_amd64.deb.html
            amazon | google | mellanox | realtek | pensando) ;;
            intel)
                (
                    cd "$item"
                    for sub_item in *; do
                        case "$sub_item" in
                        # 有 e100.ko e1000文件夹 e1000e文件夹
                        e100* | lib* | *vf | idpf) ;;
                        *) rm -rf $sub_item ;;
                        esac
                    done
                )
                ;;
            *) rm -rf $item ;;
            esac
        done
    )
    (
        cd lib/modules/*/kernel
        for item in \
            net/mac80211 \
            net/wireless \
            net/bluetooth \
            drivers/hid \
            drivers/mmc \
            drivers/mtd \
            drivers/usb \
            drivers/ssb \
            drivers/mfd \
            drivers/bcma \
            drivers/pcmcia \
            drivers/parport \
            drivers/platform \
            drivers/staging \
            drivers/net/usb \
            drivers/net/bonding \
            drivers/net/wireless \
            drivers/input/rmi4 \
            drivers/input/keyboard \
            drivers/input/touchscreen \
            drivers/bus/mhi \
            drivers/char/pcmcia \
            drivers/misc/cardreader; do
            rm -rf $item
        done
    )

    # 显示精简后的大小
    du -sh .
}

get_unix_path() {
    if is_in_windows; then
        # 输入的路径是 / 开头也没问题
        cygpath -u "$1"
    else
        printf '%s' "$1"
    fi
}

init_basearch() {
    # 设置 basearch
    if is_in_windows; then
        # x86-based PC
        # x64-based PC
        # ARM-based PC
        # ARM64-based PC

        # 三种方法都不需要管理员运行
        if false; then
            # 如果机器没有 wmic 则需要下载 wmic.ps1,但此时未判断国内外,还是用国外源
            basearch=$(wmic ComputerSystem get SystemType | grep '=' | cut -d= -f2 | cut -d- -f1)
        elif true; then
            basearch=$(reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v PROCESSOR_ARCHITECTURE |
                grep . | tail -1 | awk '{print $NF}')
        else
            # 也可以用
            basearch=$(cmd /c "if defined PROCESSOR_ARCHITEW6432 (echo %PROCESSOR_ARCHITEW6432%) else (echo %PROCESSOR_ARCHITECTURE%)")
        fi
    else
        # archlinux 云镜像没有 arch 命令
        # https://en.wikipedia.org/wiki/Uname
        basearch=$(uname -m)
    fi

    # 统一架构名称,并强制 64 位
    case "$(echo $basearch | to_lower)" in
    i?86 | x64 | x86* | amd64)
        basearch=x86_64
        basearch_alt=amd64
        ;;
    arm* | aarch64)
        basearch=aarch64
        basearch_alt=arm64
        ;;
    *) error_and_exit "Unsupported arch: $basearch" ;;
    esac
}

init_confhome() {
    # 设置 confhome
    # 未测试
    if false && [[ "$confhome" = http*://raw.githubusercontent.com/* ]]; then
        repo=$(echo $confhome | cut -d/ -f4,5)
        branch=$(echo $confhome | cut -d/ -f6)
        # 避免脚本更新时,文件不同步造成错误
        if [ -z "$commit" ]; then
            commit=$(curl -L https://api.github.com/repos/$repo/git/refs/heads/$branch |
                grep '"sha"' | grep -Eo '[0-9a-f]{40}')
        fi
        # shellcheck disable=SC2001
        confhome=$(echo "$confhome" | sed "s/main$/$commit/")
    fi

    # 设置国内代理
    # 要在使用 wmic 前设置,否则国内机器会从国外源下载 wmic.ps1
    # gitee 不支持ipv6
    # jsdelivr 有12小时缓存
    # https://github.com/XIU2/UserScript/blob/master/GithubEnhanced-High-Speed-Download.user.js#L31
    if is_in_china; then
        if [ -n "$confhome_cn" ]; then
            confhome=$confhome_cn
        elif [ -n "$github_proxy" ] && [[ "$confhome" = http*://raw.githubusercontent.com/* ]]; then
            confhome=${confhome/http:\/\//https:\/\/}
            confhome=${confhome/https:\/\/raw.githubusercontent.com/$github_proxy}
        fi
    fi
}

remove_exist_reinstall_efi_dir() {
    info "remove exist reinstall efi dir"

    local dir='' dirs=''
    if is_in_windows; then
        dirs=$(get_efi_dir_in_windows)
    else
        dirs=$(get_maybe_efi_dirs_in_linux)
    fi
    # 后期可能会将 reinstall-vmlinuz 和 reinstall-initrd 放到 efi 分区下
    # 因此也删除它们
    for dir in $dirs; do
        rm -f "$dir/reinstall-vmlinuz"
        rm -f "$dir/reinstall-initrd"
    done
    find $dirs -type f \
        \( -ipath '*/EFI/reinstall/grubx64.efi' \
        -o -ipath '*/EFI/reinstall/grubaa64.efi' \
        -o -ipath '*/EFI/reinstall/netboot.xyz.efi' \
        -o -ipath '*/EFI/reinstall/netboot.xyz-arm64.efi' \) |
        while IFS= read -r efi_file; do
            reinstall_dir=$(dirname "$efi_file")
            echo "removing $reinstall_dir"
            rm -rf "$reinstall_dir"
        done
}

#             linux                                      windows
# bios        /boot/grub*/custom.cfg                     /cygdrive/c/grub/grub.cfg
# efi         efi分区的/EFI/reinstall/grub.cfg           /cygdrive/c/grub.cfg
# efi文件夹    efi分区的/EFI/reinstall/                  /cygdrive/a/EFI/reinstall/

init_bootloader_facts() {
    if is_in_windows; then
        # windows
        if is_efi; then
            _grub_cfg=/cygdrive/$c/grub.cfg
        else
            _grub_cfg=/cygdrive/$c/grub/grub.cfg
        fi
        target_cfg=$_grub_cfg
    else
        # linux
        if is_efi; then
            efi_dir=$(get_maybe_efi_dirs_in_linux | head -1)
            _grub_cfg=$efi_dir/EFI/reinstall/grub.cfg
            target_cfg=$_grub_cfg
        else
            if is_mbr_using_grub; then
                if is_have_cmd update-grub; then
                    # alpine debian ubuntu
                    _grub_cfg=$(grep -o '[^ ]*grub.cfg' "$(get_cmd_path update-grub)" | head -1)
                else
                    # 找出主配置文件(含有menuentry|blscfg)
                    # 有没有可能在 efi 目录?
                    _grub_cfg=$(find_grub_extlinux_cfg '/boot/grub*' grub.cfg 'menuentry|blscfg')
                fi
                target_cfg=$(dirname $_grub_cfg)/custom.cfg

                if is_have_cmd grub2-mkconfig; then
                    grub=grub2
                elif is_have_cmd grub-mkconfig; then
                    grub=grub
                else
                    error_and_exit "grub not found"
                fi
            else
                # extlinux
                _extlinux_cfg=$(find_grub_extlinux_cfg /boot extlinux.conf LINUX)
                target_cfg=$_extlinux_cfg
            fi
        fi
    fi
}

# 重新生成 grub.cfg
# 因为有些机子例如hython debian的grub.cfg少了40_custom 41_custom 部分
recreate_grub_or_extlinux_cfg() {
    # 没用到原机的 grub 和 extlinux
    # 因此不需要重新生成 grub.cfg 或 extlinux.conf
    if is_efi || is_in_windows; then
        return
    fi

    if is_mbr_using_grub; then
        info "recreate grub.cfg"

        # nixos 手动执行 grub-mkconfig -o /boot/grub/grub.cfg 会丢失系统启动条目
        # 正确的方法是修改 configuration.nix 的 boot.loader.grub.extraEntries
        # 但是修改 configuration.nix 不是很好,因此改成修改 grub.cfg
        if [ -x /nix/var/nix/profiles/system/bin/switch-to-configuration ]; then
            # 生成 grub.cfg
            /nix/var/nix/profiles/system/bin/switch-to-configuration boot
            # 手动启用 41_custom
            nixos_grub_home="$(dirname "$(readlink -f "$(get_cmd_path grub-mkconfig)")")/.."
            $nixos_grub_home/etc/grub.d/41_custom >>$target_cfg
        elif is_have_cmd update-grub; then
            update-grub
        else
            $grub-mkconfig -o $target_cfg
        fi
    elif is_have_cmd update-extlinux; then
        # alpine 才有 update-extlinux
        info "recreate extlinux.conf"
        update-extlinux
    else
        error_and_exit "unsupported bootloader."
    fi
}

# 删除之前的 reinstall 启动项
remove_exist_reinstall() {
    info "remove exist reinstall"

    rm -f /reinstall-vmlinuz /reinstall-initrd
    rm -f /boot/reinstall-vmlinuz /boot/reinstall-initrd
    if is_in_windows; then
        rm -f /cygdrive/$c/reinstall-vmlinuz /cygdrive/$c/reinstall-initrd
    fi

    # 使用外部 grub 时,删除外部 grub.cfg
    if ! is_use_local_grub_extlinux; then
        rm -f "$target_cfg"
    fi

    if is_in_windows; then
        if is_efi; then
            # efi
            remove_exist_reinstall_efi_dir

            bcdedit /set '{fwbootmgr}' bootsequence '{bootmgr}'
            bcdedit /enum bootmgr | grep -a -B3 'reinstall' | awk '{print $2}' | grep '{.*}' |
                xargs -I {} cmd /c bcdedit /delete {}
        else
            # bios
            id='{1c41f649-1637-52f1-aea8-f96bfebeecc8}'
            if bcdedit /enum all | grep -a "$id"; then
                bcdedit /delete "$id"
            fi
        fi
    else
        if is_efi; then
            # efi

            # 题外话
            # 1. 如果用本机的 grub,则 custom.cfg 可能在 efi 分区,也可能在 /boot 分区
            # 2. 有可能没有 /boot 文件夹
            #    如果 nixos 的 efi 挂载到 /efi,则不会生成 /boot 文件夹
            # 3. find 不存在的路径会报错
            remove_exist_reinstall_efi_dir

            install_pkg efibootmgr
            efibootmgr | grep -q 'BootNext:' && efibootmgr --quiet --delete-bootnext
            efibootmgr | grep_efi_entry | grep 'reinstall' | grep_efi_index |
                xargs -I {} efibootmgr --quiet --bootnum {} --delete-bootnum
        else
            # bios

            # 删除 reinstall 条目
            if [ -f "$target_cfg" ]; then
                sed -i "/^$BOOT_ENTEY_START_MARK/,/^$BOOT_ENTEY_END_MARK/d" "$target_cfg"
            fi

            # 清除 next entry
            if is_use_local_grub; then
                $grub-editenv - unset next_entry
            elif is_use_local_extlinux; then
                extlinux --clear-once "$(dirname "$target_cfg")"
            fi

            # 重新创建 grub.cfg / extlinux.conf
            recreate_grub_or_extlinux_cfg
        fi
    fi
}

reset_and_exit() {
    from_ctrl_c=${1:-false}

    # info
    if $from_ctrl_c; then
        info "Caught Ctrl+C, reseting..."
    fi

    # 清除
    remove_exist_reinstall
    rm -rf "$tmp"
    echo "reset done."

    # 退出
    if $from_ctrl_c; then
        exit 1
    else
        exit 0
    fi
}

# 脚本入口

# windows 环境下的额外初始化
if is_in_windows; then
    # win系统盘
    c=$(echo $SYSTEMDRIVE | cut -c1)

    # 64位系统 + 32位cmd/cygwin,需要添加 PATH,否则找不到64位系统程序,例如bcdedit
    sysnative=$(cygpath -u $WINDIR\\Sysnative)
    if [ -d $sysnative ]; then
        PATH=$PATH:$sysnative
    fi

    # 更改 windows 命令输出语言为英文
    # chcp 会清屏
    mode.com con cp select=437 >/dev/null

    # 为 windows 程序输出删除 cr
    for exe in $WINDOWS_EXES; do
        # 如果我们覆写了 wmic(),则先将 wmic() 重命名为 _wmic()
        if get_function $exe >/dev/null 2>&1; then
            eval "_$(get_function $exe)"
        fi
        # 使用以下方法重新生成 wmic()
        # 调用链:wmic() -> run_with_del_cr(wmic) -> _wmic() -> command wmic
        eval "$exe(){ $(get_function_content run_with_del_cr_template | sed "s/\$exe/$exe/g") }"
    done
fi

# 检查 root
if is_in_windows; then
    # 64位系统 + 32位cmd/cygwin,运行 openfiles 报错:目标系统必须运行 32 位的操作系统
    if ! fltmc >/dev/null 2>&1; then
        error_and_exit "Please run as administrator."
    fi
else
    if [ "$EUID" -ne 0 ]; then
        error_and_exit "Please run as root."
    fi
fi

# 不支持 Live OS 下运行
if mount | grep -q 'tmpfs on / type tmpfs'; then
    error_and_exit "Can't run this script in Live OS."
fi

# 不支持容器虚拟化
if is_in_container; then
    error_and_exit "Not Supported OS in Container.\nPlease use https://github.com/LloydAsp/OsMutation"
fi

# 不支持安全启动
if is_secure_boot_enabled; then
    error_and_exit "Please disable secure boot first."
fi

# 整理参数
long_opts=
for o in ci installer debug minimal allow-ping force-cn help \
    add-driver: \
    hold: sleep: \
    iso: \
    image-name: \
    boot-wim: \
    img: \
    cloud-data: \
    lang: \
    passwd: password: \
    ssh-port: \
    ssh-key: public-key: \
    rdp-port: \
    web-port: http-port: \
    allow-ping: \
    commit: \
    frpc-conf: frpc-config: \
    target-disk: \
    force-boot-mode: \
    force-old-windows-setup:; do
    [ -n "$long_opts" ] && long_opts+=,
    long_opts+=$o
done

# 使用 getopt 解析参数
if ! ORIGINAL_OPTS=$(getopt -n $0 -o "h,x" --long "$long_opts" -- "$@"); then
    exit 1
fi

# 第一遍扫描,验证要安装的系统和版本
eval set -- "$ORIGINAL_OPTS"
while true; do
    case "$1" in
    -x | --debug)
        set -x
        shift
        ;;
    --)
        shift
        verify_os_name "$@"
        break
        ;;
    *)
        shift
        ;;
    esac
done

# 初始化重要变量
# wmic 随时会用到
# wmic 需要下载 wmic.ps1,需要从 confhome 得到,要先将 confhome 改成国内
# 而 wmic.ps1 又放在 $tmp 目录,因此要先创建临时目录
# 处理 --frpc-config 时会下载文件,因此在处理参数前就创建临时目录
mkdir_clear "$tmp"
init_basearch
init_confhome
init_bootloader_facts

if [ "$distro" = reset ]; then
    reset_and_exit
fi

# 安装必备组件
install_pkg curl grep

# 第二遍扫描,处理参数
eval set -- "$ORIGINAL_OPTS"
# shellcheck disable=SC2034
while true; do
    case "$1" in
    -x | --debug)
        # 第一遍扫描已处理
        shift
        ;;
    -h | --help)
        usage_and_exit
        ;;
    --commit)
        commit=$2
        shift 2
        ;;
    --ci)
        cloud_image=1
        unset installer
        shift
        ;;
    --installer)
        installer=1
        unset cloud_image
        shift
        ;;
    --minimal)
        minimal=1
        shift
        ;;
    --allow-ping)
        allow_ping=1
        shift
        ;;
    --force-cn)
        # 仅为了方便测试
        force_cn=1
        shift
        ;;
    --hold | --sleep)
        if ! { [ "$2" = 0 ] || [ "$2" = 1 ] || [ "$2" = 2 ]; }; then
            error_and_exit "Invalid $1 value: $2"
        fi
        hold=$2
        shift 2
        ;;
    --frpc-conf | --frpc-config)
        [ -n "$2" ] || error_and_exit "Need value for $1"

        case "$(to_lower <<<"$2")" in
        http://* | https://*)
            frpc_config_url=$2
            frpc_config=$tmp/frpc.conf
            # 用 file 识别文件类型?
            if ! curl -L "$frpc_config_url" -o "$frpc_config"; then
                error_and_exit "Can't get frpc config from $frpc_config_url"
            fi
            ;;
        *)
            # windows 路径转换
            if ! { frpc_config=$(get_unix_path "$2") && [ -f "$frpc_config" ]; }; then
                error_and_exit "File not exists: $2"
            fi
            ;;
        esac

        # 转为绝对路径
        frpc_config=$(readlink -f "$frpc_config")

        shift 2
        ;;
    --force-boot-mode)
        if ! { [ "$2" = bios ] || [ "$2" = efi ]; }; then
            error_and_exit "Invalid $1 value: $2"
        fi
        force_boot_mode=$2
        shift 2
        ;;
    --passwd | --password)
        [ -n "$2" ] || error_and_exit "Need value for $1"
        password=$2
        shift 2
        ;;
    --ssh-key | --public-key)
        ssh_key_error_and_exit() {
            error "$1"
            cat <=1g
# el7 aarch64 >=1.5g
# el8/9/fedora 任何架构 >=2g
if is_netboot_xyz ||
    { ! is_use_cloud_image && {
        [ "$distro" = "alpine" ] || is_distro_like_debian ||
            { is_distro_like_redhat && [ $releasever -eq 7 ] && [ $ram_size -ge 1024 ] && [ $basearch = "x86_64" ]; } ||
            { is_distro_like_redhat && [ $releasever -eq 7 ] && [ $ram_size -ge 1536 ] && [ $basearch = "aarch64" ]; } ||
            { is_distro_like_redhat && [ $releasever -ge 8 ] && [ $ram_size -ge 2048 ]; }
    }; }; then
    setos nextos $distro $releasever
else
    # alpine 作为中间系统时,使用最新版
    alpine_ver_for_trans=$(get_latest_distro_releasever alpine)
    setos finalos $distro $releasever
    setos nextos alpine $alpine_ver_for_trans
fi

# 有的机器开启了 kexec,例如腾讯云轻量 debian,要禁用
if [ -f /etc/default/kexec ]; then
    sed -i 's/LOAD_KEXEC=true/LOAD_KEXEC=false/' /etc/default/kexec
fi

# 一切就绪,正式下载内核和添加引导项
# 先删除之前的启动项,再设置 trap
# 暂时不用 trap,因为 bat 下无效
remove_exist_reinstall
# trap 'reset_and_exit true' SIGINT

# 下载 netboot.xyz / 内核
# shellcheck disable=SC2154
if is_netboot_xyz; then
    if is_efi; then
        if is_in_windows; then
            add_efi_entry_in_windows $nextos_efi
        else
            add_efi_entry_in_linux $nextos_efi
        fi
    else
        curl -Lo /reinstall-vmlinuz $nextos_vmlinuz
    fi
else
    # 下载 nextos 内核
    info download vmlnuz and initrd
    curl -Lo /reinstall-vmlinuz $nextos_vmlinuz
    curl -Lo /reinstall-initrd $nextos_initrd
    if is_use_firmware; then
        curl -Lo /reinstall-firmware $nextos_firmware
    fi
fi

# 修改 alpine debian kali initrd
if [ "$nextos_distro" = alpine ] || is_distro_like_debian "$nextos_distro"; then
    mod_initrd
fi

# 将内核/netboot.xyz.lkrn 放到正确的位置
if false && is_need_boot_vmlinuz; then
    if is_in_windows; then
        cp -f /reinstall-vmlinuz /cygdrive/$c/
        is_have_initrd && cp -f /reinstall-initrd /cygdrive/$c/
    else
        if is_os_in_btrfs && is_os_in_subvol; then
            cp_to_btrfs_root /reinstall-vmlinuz
            is_have_initrd && cp_to_btrfs_root /reinstall-initrd
        fi
    fi
fi

# 需要使用 vmlinuz/initrd 引导的情况
if is_need_boot_vmlinuz; then
    # win 使用外部 grub
    if is_in_windows; then
        install_grub_win
    else
        # linux efi 使用外部 grub,因为
        # 1. 原系统 grub 可能没有去除 aarch64 内核 magic number 校验
        # 2. 原系统可能不是用 grub
        if is_efi; then
            install_grub_linux_efi
        fi
    fi

    # 找到 /reinstall-vmlinuz /reinstall-initrd 的绝对路径
    if is_in_windows; then
        # dir=/cygwin/
        dir=$(cygpath -m / | cut -d: -f2-)/
    else
        # extlinux + 单独的 boot 分区
        # 把内核文件放在 extlinux.conf 所在的目录
        if is_use_local_extlinux && is_boot_in_separate_partition; then
            dir=
        else
            # 获取当前系统根目录在 btrfs 中的绝对路径
            if is_os_in_btrfs; then
                # btrfs subvolume show /
                # 输出可能是 / 或 root 或 @/.snapshots/1/snapshot
                dir=$(btrfs subvolume show / | head -1)
                if ! [ "$dir" = / ]; then
                    dir="/$dir/"
                fi
            else
                dir=/
            fi
        fi
    fi

    vmlinuz=${dir}reinstall-vmlinuz
    initrd=${dir}reinstall-initrd
    firmware=${dir}reinstall-firmware

    # 设置 linux initrd 命令
    if is_use_local_extlinux; then
        linux_cmd=LINUX
        initrd_cmd=INITRD
    else
        if is_netboot_xyz; then
            linux_cmd=linux16
            initrd_cmd=initrd16
        else
            linux_cmd=linux
            initrd_cmd=initrd
        fi
    fi

    # 设置 cmdlind initrds
    if ! is_netboot_xyz; then
        find_main_disk
        build_cmdline

        initrds="$initrd"
        if is_use_firmware; then
            initrds+=" $firmware"
        fi
    fi

    if is_use_local_extlinux; then
        info extlinux
        echo "$target_cfg"
        extlinux_dir="$(dirname "$target_cfg")"

        # 不起作用
        # 好像跟 extlinux --once 有冲突
        sed -i "/^MENU HIDDEN/d" "$target_cfg"
        sed -i "/^TIMEOUT /d" "$target_cfg"

        del_empty_lines <$target_cfg

        get_function_content load_grubenv_if_not_loaded >>$target_cfg

        # 原系统为 openeuler 云镜像,需要添加 --unrestricted,否则要输入密码
        del_empty_lines <>$target_cfg

        # 设置重启引导项
        if is_use_local_grub; then
            $grub-reboot "$(get_entry_name)"
        fi
    fi
fi

info 'info'
echo "$distro $releasever"

case "$distro" in
windows) username=administrator ;;
netboot.xyz) username= ;;
dd | *) username=root ;;
esac

if [ -n "$username" ]; then
    echo "Username: $username"
    if [ -n "$ssh_keys" ]; then
        echo "Public Key: $ssh_keys"
    else
        echo "Password: $password"
    fi
fi

if is_netboot_xyz; then
    echo 'Reboot to start netboot.xyz.'
elif is_alpine_live; then
    echo 'Reboot to start Alpine Live OS.'
elif is_use_dd; then
    if [ -n "$cloud_data" ]; then
        echo "Cloud Data: $cloud_data"
        echo "Cloud Data Files: $cloud_data_files"
    fi
    show_dd_password_tips
    echo 'Reboot to start DD.'
elif [ "$distro" = fnos ]; then
    echo "Special note for FNOS:"
    echo "Reboot to start the installation."
    echo "SSH login is disabled when installation completed."
    echo "You need to config the account and password on http://SERVER_IP:5666 as soon as possible."
    echo
    echo "飞牛 OS 注意事项:"
    echo "重启后开始安装。"
    echo "安装完成后不支持 SSH 登录。"
    echo "你需要尽快在 http://SERVER_IP:5666 配置账号密码。"
else
    echo "Reboot to start the installation."
fi

if is_in_windows; then
    echo 'You can run this command to reboot:'
    echo 'shutdown /r /t 0'
fi

echo
echo "If you want to revert all changes made by this script, run \"$reinstall_____ reset\""
echo


================================================
FILE: resize.sh
================================================
#!/bin/bash
PATH="/usr/sbin:/usr/bin"

update_part() {
    partx -u "$1"
    udevadm trigger
    udevadm settle
}

# el 自带 fdisk parted (el7的part不支持在线扩容)
# ubuntu 自带 fdisk growpart

# 删除分区用
# el/ubuntu fdisk

# 扩容分区用
# el7 grownparted 额外安装
# el8/9/fedora parted
# ubuntu grownpart

# 找出主硬盘
root_drive=$(mount | awk '$3=="/" {print $1}')
xda=$(lsblk -r --inverse "$root_drive" | grep -w disk | awk '{print $1}')

# 删除 installer 分区
installer_num=$(readlink -f /dev/disk/by-label/installer | grep -o '[0-9]*$')
if [ -n "$installer_num" ]; then
    # 要添加 LC_NUMERIC 或者将%转义成\%才能在cron里正确运行
    # locale -a 不一定有"en_US.UTF-8",但肯定有"C.UTF-8"
    LC_NUMERIC="C.UTF-8"
    printf "d\n%s\nw" "$installer_num" | fdisk "/dev/$xda"
    update_part "/dev/$xda"
fi

# 找出现在的最后一个分区,也就是系统分区
# el7 的 lsblk 没有 --sort,所以用其他方法
# shellcheck disable=2012
part_num=$(ls -1v "/dev/$xda"* | tail -1 | grep -o '[0-9]*$')
part_fstype=$(lsblk -no FSTYPE "/dev/$xda"*"$part_num")

# 扩容分区
# ubuntu 和 el7 用 growpart,其他用 parted
# el7 不能用parted在线扩容,而fdisk扩容会改变 PARTUUID,所以用 growpart
if grep -E -i 'centos:7|ubuntu' /etc/os-release; then
    growpart "/dev/$xda" "$part_num"
else
    printf 'yes\n100%%' | parted "/dev/$xda" resizepart "$part_num" ---pretend-input-tty
fi
update_part "/dev/$xda"

# 扩容最后一个分区的文件系统
case $part_fstype in
xfs) xfs_growfs / ;;
ext*) resize2fs "/dev/$xda"*"$part_num" ;;
btrfs) btrfs filesystem resize max / ;;
esac
update_part "/dev/$xda"

# 删除脚本自身
rm -f /resize.sh /etc/cron.d/resize


================================================
FILE: trans.sh
================================================
#!/bin/ash
# shellcheck shell=dash
# shellcheck disable=SC2086,SC3047,SC3036,SC3010,SC3001,SC3060
# alpine 默认使用 busybox ash
# 注意 bash 和 ash 以下语句结果不同
# [[ a = '*a' ]] && echo 1

# 出错后停止运行,将进入到登录界面,防止失联
set -eE

# 用于判断 reinstall.sh 和 trans.sh 是否兼容
# shellcheck disable=SC2034
SCRIPT_VERSION=4BACD833-A585-23BA-6CBB-9AA4E08E0004

TRUE=0
FALSE=1
EFI_UUID=C12A7328-F81F-11D2-BA4B-00A0C93EC93B

error() {
    color='\e[31m'
    plain='\e[0m'
    echo -e "${color}***** ERROR *****${plain}" >&2
    echo -e "${color}$*${plain}" >&2
}

info() {
    color='\e[32m'
    plain='\e[0m'
    local msg

    if [ "$1" = false ]; then
        shift
        msg=$*
    else
        msg=$(echo "$*" | to_upper)
    fi

    echo -e "${color}***** $msg *****${plain}" >&2
}

warn() {
    color='\e[33m'
    plain='\e[0m'
    echo -e "${color}Warning: $*${plain}" >&2
}

error_and_exit() {
    error "$@"
    echo "Run '/trans.sh' to retry." >&2
    echo "Run '/trans.sh alpine' to install Alpine Linux instead." >&2
    exit 1
}

trap_err() {
    line_no=$1
    ret_no=$2

    error_and_exit "$(
        echo "Line $line_no return $ret_no"
        if [ -f "/trans.sh" ]; then
            sed -n "$line_no"p /trans.sh
        fi
    )"
}

is_run_from_locald() {
    [[ "$0" = "/etc/local.d/*" ]]
}

add_community_repo() {
    # 先检查原来的repo是不是egde
    if grep -q '^http.*/edge/main$' /etc/apk/repositories; then
        alpine_ver=edge
    else
        alpine_ver=v$(cut -d. -f1,2 >/etc/apk/repositories
    fi
}

# 有时网络问题下载失败,导致脚本中断
# 因此需要重试
apk() {
    retry 5 command apk "$@" >&2
}

show_url_in_args() {
    while [ $# -gt 0 ]; do
        case "$1" in
        [Hh][Tt][Tt][Pp][Ss]://* | [Hh][Tt][Tt][Pp]://* | [Mm][Aa][Gg][Nn][Ee][Tt]:*) echo "$1" ;;
        esac
        shift
    done
}

# 在没有设置 set +o pipefail 的情况下,限制下载大小:
# retry 5 command wget | head -c 1048576 会触发 retry,下载 5 次
# command wget "$@" --tries=5 | head -c 1048576 不会触发 wget 自带的 retry,只下载 1 次
wget() {
    show_url_in_args "$@" >&2
    if command wget 2>&1 | grep -q BusyBox; then
        # busybox wget 没有重试功能
        # 好像默认永不超时
        retry 5 command wget "$@" -T 10
    else
        # 原版 wget 自带重试功能
        command wget --tries=5 --progress=bar:force "$@"
    fi
}

is_have_cmd() {
    # command -v 包括脚本里面的方法
    is_have_cmd_on_disk / "$1"
}

is_have_cmd_on_disk() {
    local os_dir=$1
    local cmd=$2

    for bin_dir in /bin /sbin /usr/bin /usr/sbin; do
        if [ -f "$os_dir$bin_dir/$cmd" ]; then
            return
        fi
    done
    return 1
}

is_num() {
    echo "$1" | grep -Exq '[0-9]*\.?[0-9]*'
}

retry() {
    local max_try=$1
    shift

    if is_num "$1"; then
        local interval=$1
        shift
    else
        local interval=5
    fi

    for i in $(seq $max_try); do
        if "$@"; then
            return
        else
            ret=$?
            if [ $i -ge $max_try ]; then
                return $ret
            fi
            sleep $interval
        fi
    done
}

get_url_type() {
    if [[ "$1" = magnet:* ]]; then
        echo bt
    else
        echo http
    fi
}

is_magnet_link() {
    [[ "$1" = magnet:* ]]
}

download() {
    url=$1
    path=$2

    # 有ipv4地址无ipv4网关的情况下,aria2可能会用ipv4下载,而不是ipv6
    # axel 在 lightsail 上会占用大量cpu
    # https://download.opensuse.org/distribution/leap/15.5/appliances/openSUSE-Leap-15.5-Minimal-VM.x86_64-kvm-and-xen.qcow2
    # https://aria2.github.io/manual/en/html/aria2c.html#cmdoption-o

    # 阿里云源限速,而且检测 user-agent 禁止 axel/aria2 下载
    # aria2 默认 --max-tries 5

    # 默认 --max-tries=5,但以下情况服务器出错,aria2不会重试,而是直接返回错误
    # 因此添加 for 循环
    #     [ERROR] CUID#7 - Download aborted. URI=https://aka.ms/manawindowsdrivers
    # Exception: [AbstractCommand.cc:351] errorCode=1 URI=https://aka.ms/manawindowsdrivers
    #   -> [SocketCore.cc:1019] errorCode=1 SSL/TLS handshake failure:  `not signed by known authorities or invalid'

    # 用 if 的话,报错不会中断脚本
    # if aria2c xxx; then
    #     return
    # fi

    # --user-agent=Wget/1.21.1 \
    # --retry-wait 5

    # 检测大小时已经下载了种子
    if [ "$(get_url_type "$url")" = bt ]; then
        torrent="$(get_torrent_path_by_magnet $url)"
        if ! [ -f "$torrent" ]; then
            download_torrent_by_magnet "$url" "$torrent"
        fi
        url=$torrent
    fi

    # intel 禁止了 aria2 下载驱动
    # intel 禁止了 wget 下载网页内容
    # 腾讯云 virtio 驱动也禁止了 aria2 下载

    # -o 设置 http 下载文件名
    # -O 设置 bt 首个文件的文件名
    aria2c "$url" \
        -d "$(dirname "$path")" \
        -o "$(basename "$path")" \
        -O "1=$(basename "$path")" \
        -U curl/7.54.1

    # opensuse 官方镜像支持 metalink
    # aira2 无法重命名用 metalink 下载的文件
    # 需用以下方法重命名
    if head -c 1024 "$path" | grep -Fq 'urn:ietf:params:xml:ns:metalink'; then
        real_file=$(tr -d '\n' <"$path" | sed -E 's|.*/dev/null || true
    fi

    # partx
    # https://access.redhat.com/solutions/199573
    if is_have_cmd partx; then
        partx -u /dev/$xda
    fi

    # mdev
    # mdev 不会删除 /dev/disk/ 的旧分区,因此手动删除
    # 如果 rm -rf 的时候刚好 mdev 在创建链接,rm -rf 会报错 Directory not empty
    # 因此要先停止 mdev 服务
    # 还要删除 /dev/$xda*?
    ensure_service_stopped mdev
    # 即使停止了 mdev,有时也会报 Directory not empty,因此添加 retry
    retry 5 rm -rf /dev/disk/*

    # 没挂载 modloop 时会提示
    # modprobe: can't change directory to '/lib/modules': No such file or directory
    # 因此强制不显示上面的提示
    mdev -sf 2>/dev/null
    ensure_service_started mdev 2>/dev/null
    sleep 1
}

is_efi() {
    if [ -n "$force_boot_mode" ]; then
        [ "$force_boot_mode" = efi ]
    else
        [ -d /sys/firmware/efi/ ]
    fi
}

is_use_cloud_image() {
    [ -n "$cloud_image" ] && [ "$cloud_image" = 1 ]
}

is_allow_ping() {
    [ -n "$allow_ping" ] && [ "$allow_ping" = 1 ]
}

setup_nginx() {
    apk add nginx
    # shellcheck disable=SC2154
    wget $confhome/logviewer.html -O /logviewer.html
    wget $confhome/logviewer-nginx.conf -O /etc/nginx/http.d/default.conf

    if [ -z "$web_port" ]; then
        web_port=80
    fi
    sed -i "s/@WEB_PORT@/$web_port/gi" /etc/nginx/http.d/default.conf

    # rc-service -q nginx start
    if pgrep nginx >/dev/null; then
        nginx -s reload
    else
        nginx
    fi
}

setup_websocketd() {
    apk add websocketd
    wget $confhome/logviewer.html -O /tmp/index.html
    apk add coreutils

    if [ -z "$web_port" ]; then
        web_port=80
    fi

    pkill websocketd || true
    # websocketd 遇到 \n 才推送,因此要转换 \r 为 \n
    websocketd --port "$web_port" --loglevel=fatal --staticdir=/tmp \
        stdbuf -oL -eL sh -c "tail -fn+0 /reinstall.log | tr '\r' '\n' | grep -Fiv -e password -e token" &
}

get_approximate_ram_size() {
    # lsmem 需要 util-linux
    if false && is_have_cmd lsmem; then
        ram_size=$(lsmem -b 2>/dev/null | grep 'Total online memory:' | awk '{ print $NF/1024/1024 }')
    fi

    if [ -z $ram_size ]; then
        ram_size=$(free -m | awk '{print $2}' | sed -n '2p')
    fi

    echo "$ram_size"
}

setup_web_if_enough_ram() {
    total_ram=$(get_approximate_ram_size)
    # 512内存才安装
    if [ "$total_ram" -ge 400 ]; then
        # lighttpd 虽然运行占用内存少,但安装占用空间大
        # setup_lighttpd
        # setup_nginx
        setup_websocketd
    fi
}

setup_lighttpd() {
    apk add lighttpd
    ln -sf /reinstall.html /var/www/localhost/htdocs/index.html
    rc-service -q lighttpd start
}

get_ttys() {
    prefix=$1
    # shellcheck disable=SC2154
    wget $confhome/ttys.sh -O- | sh -s $prefix
}

find_xda() {
    # 出错后再运行脚本,硬盘可能已经格式化,之前记录的分区表 id 无效
    # 因此找到 xda 后要保存 xda 到 /configs/xda

    # 先读取之前保存的
    if xda=$(get_config xda 2>/dev/null) && [ -n "$xda" ]; then
        return
    fi

    # 防止 $main_disk 为空
    if [ -z "$main_disk" ]; then
        error_and_exit "cmdline main_disk is empty."
    fi

    # busybox fdisk/lsblk/blkid 不显示 mbr 分区表 id
    # 可用以下工具:
    # fdisk 在 util-linux-misc 里面,占用大
    # sfdisk 占用小
    # lsblk
    # blkid

    tool=sfdisk

    is_have_cmd $tool && need_install_tool=false || need_install_tool=true
    if $need_install_tool; then
        apk add $tool
    fi

    if [ "$tool" = sfdisk ]; then
        # sfdisk
        for disk in $(get_all_disks); do
            if sfdisk --disk-id "/dev/$disk" | sed 's/0x//' | grep -ix "$main_disk"; then
                xda=$disk
                break
            fi
        done
    else
        # lsblk
        xda=$(lsblk --nodeps -rno NAME,PTUUID | grep -iw "$main_disk" | awk '{print $1}')
    fi

    if [ -n "$xda" ]; then
        set_config xda "$xda"
    else
        error_and_exit "Could not find xda: $main_disk"
    fi

    if $need_install_tool; then
        apk del $tool
    fi
}

get_all_disks() {
    # shellcheck disable=SC2010
    ls /sys/block/ | grep -Ev '^(loop|sr|nbd)'
}

extract_env_from_cmdline() {
    # 提取 finalos/extra 到变量
    for prefix in finalos extra; do
        while read -r line; do
            if [ -n "$line" ]; then
                key=$(echo $line | cut -d= -f1)
                value=$(echo $line | cut -d= -f2-)
                eval "$key='$value'"
            fi
        done < <(xargs -n1 $file
Reinstalling...
To view logs run:
tail -fn+1 /reinstall.log
EOF
    fi
}

umount_all() {
    dirs="/mnt /os /iso /wim /installer /nbd /nbd-boot /nbd-efi /nbd-test /root /nix"
    regex=$(echo "$dirs" | sed 's, ,|,g')
    if mounts=$(mount | grep -Ew "on $regex" | awk '{print $3}' | tac); then
        for mount in $mounts; do
            echo "umount $mount"
            umount $mount
        done
    fi
}

# 可能脚本不是首次运行,先清理之前的残留
clear_previous() {
    if is_have_cmd vgchange; then
        umount -R /os /nbd || true
        vgchange -an
        apk add device-mapper
        dmsetup remove_all
    fi
    disconnect_qcow
    # 安装 arch 有 gpg-agent 进程驻留
    pkill gpg-agent || true
    rc-service -q --ifexists --ifstarted nix-daemon stop
    swapoff -a
    umount_all

    # 以下情况 umount -R /1 会提示 busy
    # mount /file1 /1
    # mount /1/file2 /2
}

# virt-what 自动安装 dmidecode,因此同时缓存
cache_dmi_and_virt() {
    if ! [ "$_dmi_and_virt_cached" = 1 ]; then
        apk add virt-what

        # 区分 kvm 和 virtio,原因:
        # 1. 阿里云 c8y virt-what 不显示 kvm
        # 2. 不是所有 kvm 都需要 virtio 驱动,例如 aws nitro
        # 3. virt-what 不会检测 virtio
        _virt=$(
            virt-what

            # hyper-v 环境下 modprobe virtio_scsi 也会创建 /sys/bus/virtio/drivers/virtio_scsi
            # 因此用 devices 判断更准确,有设备时才有 /sys/bus/virtio/drivers/*
            # 或者加上 lspci 检测?

            # 不要用 ls /sys/bus/virtio/devices/* && echo virtio
            # 因为有可能返回值不为 0 而中断脚本
            if ls /sys/bus/virtio/devices/* >/dev/null 2>&1; then
                echo virtio
            fi
        )

        _dmi=$(dmidecode | grep -E '(Manufacturer|Asset Tag|Vendor): ' | awk -F': ' '{print $2}')
        _dmi_and_virt_cached=1
        apk del virt-what
    fi
}

is_virt() {
    cache_dmi_and_virt
    [ -n "$_virt" ]
}

is_virt_contains() {
    cache_dmi_and_virt
    echo "$_virt" | grep -Eiwq "$1"
}

is_dmi_contains() {
    # Manufacturer: Alibaba Cloud
    # Manufacturer: Tencent Cloud
    # Manufacturer: Huawei Cloud
    # Asset Tag: OracleCloud.com
    # Vendor: Amazon EC2
    # Manufacturer: Amazon EC2
    # Asset Tag: Amazon EC2
    cache_dmi_and_virt
    echo "$_dmi" | grep -Eiwq "$1"
}

cache_lspci() {
    if [ -z "$_lspci" ]; then
        apk add pciutils
        _lspci=$(lspci)
        apk del pciutils
    fi
}

is_lspci_contains() {
    cache_lspci
    echo "$_lspci" | grep -Eiwq "$1"
}

get_config() {
    cat "/configs/$1"
}

set_config() {
    printf '%s' "$2" >"/configs/$1"
}

# ubuntu 安装版、el/ol 安装版不使用该密码
get_password_linux_sha512() {
    get_config password-linux-sha512
}

get_password_windows_administrator_base64() {
    get_config password-windows-administrator-base64
}

get_password_plaintext() {
    get_config password-plaintext
}

is_password_plaintext() {
    get_password_plaintext >/dev/null 2>&1
}

show_netconf() {
    grep -r . /dev/netconf/
}

get_ra_to() {
    if [ -z "$_ra" ]; then
        apk add ndisc6
        # 有时会重复收取,所以设置收一份后退出
        echo "Gathering network info..."
        # shellcheck disable=SC2154
        _ra="$(rdisc6 -1 "$ethx")"
        apk del ndisc6

        # 显示网络配置
        info "Network info:"
        echo
        echo "$_ra" | cat -n
        echo
        ip addr | cat -n
        echo
        show_netconf | cat -n
        echo
    fi
    eval "$1='$_ra'"
}

get_netconf_to() {
    case "$1" in
    slaac | dhcpv6 | rdnss | other) get_ra_to ra ;;
    esac

    # shellcheck disable=SC2154
    # debian initrd 没有 xargs
    case "$1" in
    slaac) echo "$ra" | grep 'Autonomous address conf' | grep -q Yes && res=1 || res=0 ;;
    dhcpv6) echo "$ra" | grep 'Stateful address conf' | grep -q Yes && res=1 || res=0 ;;
    rdnss) res=$(echo "$ra" | grep 'Recursive DNS server' | cut -d: -f2-) ;;
    other) echo "$ra" | grep 'Stateful other conf' | grep -q Yes && res=1 || res=0 ;;
    *) res=$(cat /dev/netconf/$ethx/$1) ;;
    esac

    eval "$1='$res'"
}

is_any_ipv4_has_internet() {
    grep -q 1 /dev/netconf/*/ipv4_has_internet
}

is_in_china() {
    grep -q 1 /dev/netconf/*/is_in_china
}

# 有 dhcpv4 不等于有网关,例如 vultr 纯 ipv6
# 没有 dhcpv4 不等于是静态ip,可能是没有 ip
is_dhcpv4() {
    if ! is_ipv4_has_internet || should_disable_dhcpv4; then
        return 1
    fi

    get_netconf_to dhcpv4
    # shellcheck disable=SC2154
    [ "$dhcpv4" = 1 ]
}

is_staticv4() {
    if ! is_ipv4_has_internet; then
        return 1
    fi

    if ! is_dhcpv4; then
        get_netconf_to ipv4_addr
        get_netconf_to ipv4_gateway
        if [ -n "$ipv4_addr" ] && [ -n "$ipv4_gateway" ]; then
            return 0
        fi
    fi
    return 1
}

is_staticv6() {
    if ! is_ipv6_has_internet; then
        return 1
    fi

    if ! is_slaac && ! is_dhcpv6; then
        get_netconf_to ipv6_addr
        get_netconf_to ipv6_gateway
        if [ -n "$ipv6_addr" ] && [ -n "$ipv6_gateway" ]; then
            return 0
        fi
    fi
    return 1
}

is_dhcpv6_or_slaac() {
    get_netconf_to dhcpv6_or_slaac
    # shellcheck disable=SC2154
    [ "$dhcpv6_or_slaac" = 1 ]
}

is_ipv4_has_internet() {
    get_netconf_to ipv4_has_internet
    # shellcheck disable=SC2154
    [ "$ipv4_has_internet" = 1 ]
}

is_ipv6_has_internet() {
    get_netconf_to ipv6_has_internet
    # shellcheck disable=SC2154
    [ "$ipv6_has_internet" = 1 ]
}

should_disable_dhcpv4() {
    get_netconf_to should_disable_dhcpv4
    # shellcheck disable=SC2154
    [ "$should_disable_dhcpv4" = 1 ]
}

should_disable_accept_ra() {
    get_netconf_to should_disable_accept_ra
    # shellcheck disable=SC2154
    [ "$should_disable_accept_ra" = 1 ]
}

should_disable_autoconf() {
    get_netconf_to should_disable_autoconf
    # shellcheck disable=SC2154
    [ "$should_disable_autoconf" = 1 ]
}

is_slaac() {
    # 如果是静态(包括自动获取到 IP 但无法联网而切换成静态)直接返回 1,不考虑 ra
    # 防止部分机器slaac/dhcpv6获取的ip/网关无法上网

    # 有可能 ra 的 dhcpv6/slaac 是打开的,但实测无法获取到 ipv6 地址
    # is_dhcpv6_or_slaac 是实测结果,因此如果实测不通过,也返回 1

    # 不要判断 is_staticv6,因为这会导致死循环
    if ! is_ipv6_has_internet || ! is_dhcpv6_or_slaac || should_disable_accept_ra || should_disable_autoconf; then
        return 1
    fi
    get_netconf_to slaac
    # shellcheck disable=SC2154
    [ "$slaac" = 1 ]
}

is_dhcpv6() {
    # 如果是静态(包括自动获取到 IP 但无法联网而切换成静态)直接返回 1,不考虑 ra
    # 防止部分机器slaac/dhcpv6获取的ip/网关无法上网

    # 有可能 ra 的 dhcpv6/slaac 是打开的,但实测无法获取到 ipv6 地址
    # is_dhcpv6_or_slaac 是实测结果,因此如果实测不通过,也返回 1

    # 不要判断 is_staticv6,因为这会导致死循环
    if ! is_ipv6_has_internet || ! is_dhcpv6_or_slaac || should_disable_accept_ra || should_disable_autoconf; then
        return 1
    fi
    get_netconf_to dhcpv6

    # shellcheck disable=SC2154
    # 甲骨文即使没有添加 IPv6 地址,RA DHCPv6 标志也是开的
    # 部分系统开机需要等 DHCPv6 超时
    # 这种情况需要禁用 DHCPv6
    if [ "$dhcpv6" = 1 ] && ! ip -6 -o addr show scope global dev "$ethx" | grep -q .; then
        echo 'DHCPv6 flag is on, but DHCPv6 is not working.'
        return 1
    fi

    [ "$dhcpv6" = 1 ]
}

is_have_ipv6() {
    is_slaac || is_dhcpv6 || is_staticv6
}

is_enable_other_flag() {
    get_netconf_to other
    # shellcheck disable=SC2154
    [ "$other" = 1 ]
}

is_have_rdnss() {
    # rdnss 可能有几个
    get_netconf_to rdnss
    [ -n "$rdnss" ]
}

# dd 完检测到镜像是 windows 时会改写此方法
is_windows() {
    [ "$distro" = windows ]
}

# 15063 或之后才支持 rdnss
is_windows_support_rdnss() {
    [ "$build_ver" -ge 15063 ]
}

get_windows_version_from_windows_drive() {
    local os_dir=$1

    # https://wiki.tcl-lang.org/page/Windows+OS+name
    # https://nsis.sourceforge.io/Get_Windows_version

    # win10+ 才有 CurrentMajorVersionNumber 和 CurrentMinorVersionNumber
    # CurrentVersion            6.3
    # CurrentMajorVersionNumber  10
    # CurrentMinorVersionNumber   0

    apk add hivex
    hive=$(find_file_ignore_case $os_dir/Windows/System32/config/SOFTWARE)

    get_current_version_key() {
        hivexget "$hive" "Microsoft\Windows NT\CurrentVersion" "$1"
    }

    # nt_ver
    if { nt_ver_major=$(get_current_version_key CurrentMajorVersionNumber) &&
        nt_ver_minor=$(get_current_version_key CurrentMinorVersionNumber); } 2>/dev/null; then
        nt_ver="$nt_ver_major.$nt_ver_minor"
    else
        # en_windows_vista_sp2_x64_dvd_342267.iso
        # 安装前 CurrentVersion 是 6.0
        # 安装后 CurrentVersion 是 6.0

        # en_windows_vista_sp2_with_update_6003.23713_aio_7in1_x64_v26.01.13_by_adguard.iso
        # 安装前 CurrentVersion 是 6.0.6002.18005
        # 安装后 CurrentVersion 是 6.0

        # 添加 cut 用于兼容这两种情况
        nt_ver=$(get_current_version_key CurrentVersion | cut -d. -f1-2)
    fi

    # build_ver
    # win10 22h2 19045 的 exe/dll 版本还是 19041 的,因此要从注册表获取
    # vista sp2 iso 安装 KB4474419 后, CurrentBuild 是 6002, CurrentBuildNumber 是 6003
    build_ver=$(get_current_version_key CurrentBuildNumber)

    # rev_ver
    # 实测 win10 winver 是从 UBR 读取 revision 版本
    # vista sp2 iso 没有 UBR,后期有月度汇总更新包时才有 UBR
    if ! rev_ver=$(get_current_version_key UBR 2>/dev/null); then
        rev_ver=$(get_current_version_key BuildLabEx | cut -d. -f2)
    fi

    echo "Version: $nt_ver.$build_ver.$rev_ver" >&2
    apk del hivex
}

is_elts() {
    [ -n "$elts" ] && [ "$elts" = 1 ]
}

is_need_set_ssh_keys() {
    [ -s /configs/ssh_keys ]
}

is_need_change_ssh_port() {
    [ -n "$ssh_port" ] && ! [ "$ssh_port" = 22 ]
}

is_need_change_rdp_port() {
    [ -n "$rdp_port" ] && ! [ "$rdp_port" = 3389 ]
}

is_need_manual_set_dnsv6() {
    # 有没有可能是静态但是有 rdnss?
    ! is_have_ipv6 && return $FALSE
    is_dhcpv6 && return $FALSE
    is_staticv6 && return $TRUE
    is_slaac && ! is_enable_other_flag &&
        { ! is_have_rdnss || { is_have_rdnss && is_windows && ! is_windows_support_rdnss; }; }
}

get_current_dns() {
    mark=$(
        case "$1" in
        4) echo . ;;
        6) echo : ;;
        esac
    )
    # debian 11 initrd 没有 xargs awk
    # debian 12 initrd 没有 xargs
    if false; then
        grep '^nameserver' /etc/resolv.conf | awk '{print $2}' | grep -F "$mark" | cut -d '%' -f1
    else
        grep '^nameserver' /etc/resolv.conf | cut -d' ' -f2 | grep -F "$mark" | cut -d '%' -f1
    fi
}

to_upper() {
    tr '[:lower:]' '[:upper:]'
}

to_lower() {
    tr '[:upper:]' '[:lower:]'
}

del_cr() {
    sed 's/\r$//'
}

del_comment_lines() {
    sed '/^[[:space:]]*#/d'
}

del_empty_lines() {
    sed '/^[[:space:]]*$/d'
}

del_head_empty_lines_inplace() {
    # 从第一行直到找到 ^[:space:]
    # 这个区间内删除所有空行
    sed -i '1,/[^[:space:]]/ { /^[[:space:]]*$/d }' "$@"
}

get_part_num_by_part() {
    dev_part=$1
    echo "$dev_part" | grep -o '[0-9]*' | tail -1
}

get_fallback_efi_file_name() {
    case $(arch) in
    x86_64) echo bootx64.efi ;;
    aarch64) echo bootaa64.efi ;;
    *) error_and_exit ;;
    esac
}

del_invalid_efi_entry() {
    info "del invalid EFI entry"
    apk add lsblk efibootmgr

    efibootmgr --quiet --remove-dups

    while read -r line; do
        part_uuid=$(echo "$line" | awk -F ',' '{print $3}')
        efi_index=$(echo "$line" | grep_efi_index)
        if ! lsblk -o PARTUUID | grep -q "$part_uuid"; then
            echo "Delete invalid EFI Entry: $line"
            efibootmgr --quiet --bootnum "$efi_index" --delete-bootnum
        fi
    done < <(efibootmgr | grep 'HD(.*,GPT,')
}

# reinstall.sh 有同名方法
grep_efi_index() {
    awk '{print $1}' | sed -e 's/Boot//' -e 's/\*//'
}

# 某些机器可能不会回落到 bootx64.efi
# 阿里云 ECS 启动项有 EFI Shell
# 添加 bootx64.efi 到最后的话,会进入 EFI Shell
# 因此添加到最前面
add_default_efi_to_nvram() {
    info "add default EFI to nvram"

    apk add lsblk efibootmgr

    if efi_row=$(lsblk /dev/$xda -ro NAME,PARTTYPE,PARTUUID | grep -i "$EFI_UUID"); then
        efi_part_uuid=$(echo "$efi_row" | awk '{print $3}')
        efi_part_name=$(echo "$efi_row" | awk '{print $1}')
        efi_part_num=$(get_part_num_by_part "$efi_part_name")
        efi_file=$(get_fallback_efi_file_name)

        # 创建条目,先判断是否已经存在
        # 好像没必要先判断
        if true || ! efibootmgr | grep -i "HD($efi_part_num,GPT,$efi_part_uuid,.*)/File(\\\EFI\\\boot\\\\$efi_file)"; then
            efibootmgr --create \
                --disk "/dev/$xda" \
                --part "$efi_part_num" \
                --label "$efi_file" \
                --loader "\\EFI\\boot\\$efi_file"
        fi
    else
        # shellcheck disable=SC2154
        if [ "$confirmed_no_efi" = 1 ]; then
            echo 'Confirmed no EFI in previous step.'
        else
            # reinstall.sh 里确认过一遍,但是逻辑扇区大于 512 时,可能漏报?
            # 这里的应该会根据逻辑扇区来判断?
            echo "
Warning: This machine is currently using EFI boot, but the main hard drive does not have an EFI partition.
If this machine supports Legacy BIOS boot (CSM), you can safely restart into the new system by running the reboot command.
If this machine does not support Legacy BIOS boot (CSM), you will not be able to enter the new system after rebooting.

警告:本机目前使用 EFI 引导,但主硬盘没有 EFI 分区。
如果本机支持 Legacy BIOS 引导 (CSM),你可以运行 reboot 命令安全地重启到新系统。
如果本机不支持 Legacy BIOS 引导 (CSM),重启后将无法进入新系统。
"
            exit
        fi
    fi
}

unix2dos() {
    target=$1

    # 先原地unix2dos,出错再用cat,可最大限度保留文件权限
    if ! command unix2dos $target 2>/tmp/unix2dos.log; then
        # 出错后删除 unix2dos 创建的临时文件
        rm "$(awk -F: '{print $2}' /tmp/unix2dos.log | xargs)"
        tmp=$(mktemp)
        cp $target $tmp
        command unix2dos $tmp
        # cat 可以保留权限
        cat $tmp >$target
        rm $tmp
    fi
}

insert_into_file() {
    local file=$1
    local location=$2
    local regex_to_find=$3
    shift 3

    if ! [ -f "$file" ]; then
        error_and_exit "File not found: $file"
    fi

    # 默认 grep -E
    if [ $# -eq 0 ]; then
        set -- -E
    fi

    if [ "$location" = head ]; then
        bak=$(mktemp)
        cp $file $bak
        cat - $bak >$file
    else
        line_num=$(grep "$@" -n "$regex_to_find" "$file" | cut -d: -f1)

        found_count=$(echo "$line_num" | wc -l)
        if [ ! "$found_count" -eq 1 ]; then
            return 1
        fi

        case "$location" in
        before) line_num=$((line_num - 1)) ;;
        after) ;;
        *) return 1 ;;
        esac

        sed -i "${line_num}r /dev/stdin" "$file"
    fi
}

get_eths() {
    (
        cd /dev/netconf
        ls
    )
}

is_distro_like_debian() {
    [ "$distro" = debian ] || [ "$distro" = kali ]
}

create_ifupdown_config() {
    conf_file=$1

    rm -f $conf_file

    if is_distro_like_debian; then
        cat <>$conf_file
source /etc/network/interfaces.d/*

EOF
    fi

    # 生成 lo配置
    cat <>$conf_file
auto lo
iface lo inet loopback
EOF

    # ethx
    for ethx in $(get_eths); do
        mode=auto
        # shellcheck disable=SC2154
        if false; then
            if { [ "$distro" = debian ] && [ "$releasever" -ge 12 ]; } ||
                [ "$distro" = kali ]; then
                # alice + allow-hotplug 会有问题
                # 问题 1 debian 9/10/11/12:
                # 如果首次启动时,/etc/networking/interfaces 的 ethx 跟安装时不同
                # 即使启动 networking 服务前成功执行了 fix-eth-name.sh ,网卡也不会启动
                # 测试方法: 安装时手动修改 /etc/networking/interfaces enp3s0 为其他名字
                # 问题 2 debian 9/10/11:
                # 重启系统后会自动启动网卡,但运行 systemctl restart networking 会关闭网卡
                # 可能的原因: /lib/systemd/system/networking.service 没有 hotplug 相关内容,而 debian 12+ 有
                if [ -f /etc/network/devhotplug ] && grep -wo "$ethx" /etc/network/devhotplug; then
                    mode=allow-hotplug
                fi
            fi

            # if is_have_cmd udevadm; then
            #     enpx=$(udevadm test-builtin net_id /sys/class/net/$ethx 2>&1 | grep ID_NET_NAME_PATH= | cut -d= -f2)
            # fi
        fi

        # dmit debian 普通内核和云内核网卡名不一致,因此需要 rename
        # 安装系统时 ens18
        # 普通内核   ens18
        # 云内核     enp6s18
        # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=928923

        # 头部
        get_netconf_to mac_addr
        {
            echo
            # 这是标记,fix-eth-name 要用,不要删除
            # shellcheck disable=SC2154
            echo "# mac $mac_addr"
            echo $mode $ethx
        } >>$conf_file

        # ipv4
        if is_dhcpv4; then
            echo "iface $ethx inet dhcp" >>$conf_file

        elif is_staticv4; then
            get_netconf_to ipv4_addr
            get_netconf_to ipv4_gateway
            cat <>$conf_file
iface $ethx inet static
    address $ipv4_addr
    gateway $ipv4_gateway
EOF
            # dns
            if list=$(get_current_dns 4); then
                for dns in $list; do
                    cat <>$conf_file
    dns-nameservers $dns
EOF
                done
            fi
        fi

        # ipv6
        if is_slaac; then
            echo "iface $ethx inet6 auto" >>$conf_file

        elif is_dhcpv6; then
            # debian 13 使用 ifupdown + dhcpcd-base
            # inet/inet6 都配置成 dhcp 时,重启后 dhcpv4 会丢失
            # 手动 systemctl restart networking 后正常
            # 删除 dhcpcd-base 安装 isc-dhcp-client(类似 debian 12 升级到 13),轮到 dhcpv6 丢失
            if { [ "$distro" = debian ] && [ "$releasever" -ge 13 ]; } ||
                [ "$distro" = kali ]; then
                echo "iface $ethx inet6 auto" >>$conf_file
            else
                echo "iface $ethx inet6 dhcp" >>$conf_file
            fi

        elif is_staticv6; then
            get_netconf_to ipv6_addr
            get_netconf_to ipv6_gateway
            cat <>$conf_file
iface $ethx inet6 static
    address $ipv6_addr
    gateway $ipv6_gateway
EOF
            # debian 9
            # ipv4 支持静态 onlink 网关
            # ipv6 不支持静态 onlink 网关,需使用 post-up 添加,未测试动态
            # ipv6 也不支持直接 ip route add default via xxx onlink
            if [ "$distro" = debian ] && [ "$releasever" -le 9 ]; then
                # debian 添加 gateway 失败时不会执行 post-up
                # 因此 gateway post-up 只能二选一

                # 注释最后一行,也就是 gateway
                sed -Ei '$s/^( *)/\1# /' "$conf_file"
                cat <>$conf_file
    post-up ip route add $ipv6_gateway dev $ethx
    post-up ip route add default via $ipv6_gateway dev $ethx
EOF
            fi

            # 额外的 IPv6 地址(子网不含网关的地址)
            get_netconf_to ipv6_extra_addrs
            if [ -n "$ipv6_extra_addrs" ]; then
                (
                    IFS=','
                    for _addr in $ipv6_extra_addrs; do
                        echo "    post-up ip -6 addr add $_addr dev $ethx" >>$conf_file
                    done
                )
            fi
        fi

        # dns
        # 有 ipv6 但需设置 dns 的情况
        if is_need_manual_set_dnsv6; then
            for dns in $(get_current_dns 6); do
                cat <>$conf_file
    dns-nameserver $dns
EOF
            done
        fi

        # 禁用 ra
        if should_disable_accept_ra; then
            if [ "$distro" = alpine ]; then
                cat <>$conf_file
    pre-up echo 0 >/proc/sys/net/ipv6/conf/$ethx/accept_ra
EOF
            else
                cat <>$conf_file
    accept_ra 0
EOF
            fi
        fi

        # 禁用 autoconf
        if should_disable_autoconf; then
            if [ "$distro" = alpine ]; then
                cat <>$conf_file
    pre-up echo 0 >/proc/sys/net/ipv6/conf/$ethx/autoconf
EOF
            else
                cat <>$conf_file
    autoconf 0
EOF
            fi
        fi
    done
}

newline_to_comma() {
    tr '\n' ','
}

space_to_newline() {
    sed 's/ /\n/g'
}

trim() {
    sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'
}

quote_word() {
    sed -E 's/([^[:space:]]+)/"\1"/g'
}

quote_line() {
    awk '{print "\""$0"\""}'
}

add_space() {
    space_count=$1

    spaces=$(printf '%*s' "$space_count" '')
    sed "s/^/$spaces/"
}

# 不够严谨,谨慎使用
nix_replace() {
    local key=$1
    local value=$2
    local type=$3
    local file=$4
    local key_ value_

    key_=$(echo "$key" | sed 's \. \\\. g') # . 改成 \.

    if [ "$type" = array ]; then
        local value_="[ $value ]"
    fi

    sed -i "s/$key_ =.*/$key = $value_;/" "$file"
}

create_nixos_network_config() {
    conf_file=$1
    true >$conf_file

    # 头部
    cat <>$conf_file
networking = {
  usePredictableInterfaceNames = false;
EOF

    for ethx in $(get_eths); do
        # ipv4
        if is_staticv4; then
            get_netconf_to ipv4_addr
            get_netconf_to ipv4_gateway
            IFS=/ read -r address prefix < <(echo "$ipv4_addr")
            cat <>$conf_file
  interfaces.$ethx.ipv4.addresses = [
    {
      address = "$address";
      prefixLength = $prefix;
    }
  ];
  defaultGateway = {
    address = "$ipv4_gateway";
    interface = "$ethx";
  };
EOF
        fi

        # ipv6
        if is_staticv6; then
            get_netconf_to ipv6_addr
            get_netconf_to ipv6_gateway
            IFS=/ read -r address prefix < <(echo "$ipv6_addr")
            cat <>$conf_file
  interfaces.$ethx.ipv6.addresses = [
    {
      address = "$address";
      prefixLength = $prefix;
    }
  ];
  defaultGateway6 = {
    address = "$ipv6_gateway";
    interface = "$ethx";
  };
EOF
        fi
    done

    # 全局 dns
    need_set_dns=false
    for ethx in $(get_eths); do
        if is_staticv4 || is_staticv6 || is_need_manual_set_dnsv6; then
            need_set_dns=true
            break
        fi
    done

    if $need_set_dns; then
        cat <>$conf_file
  nameservers = [
$(get_current_dns | quote_line | add_space 4)
  ];
EOF
    fi

    # 尾部
    cat <>$conf_file
};
EOF

    # nixos 默认网络管理器是 dhcpcd
    # 但配置静态 ip 时用的是脚本
    # /nix/store/qcr1xxjdxcrnwqwrgysqpxx2aibp9fdl-unit-script-network-addresses-eth0-start/bin/network-addresses-eth0-start
    # ...
    # if out=$(ip addr replace "181.x.x.x/24" dev "eth0" 2>&1); then
    #   echo "done"
    # else
    #   echo "'ip addr replace "181.x.x.x/24" dev "eth0"' failed: $out"
    #   exit 1
    # fi
    # ...

    # 禁用 ra/autoconf
    local mode=1
    for ethx in $(get_eths); do
        if should_disable_accept_ra; then
            case "$mode" in
            1)
                cat <>$conf_file
boot.kernel.sysctl."net.ipv6.conf.$ethx.accept_ra" = false;
EOF
                ;;
            2)
                # nixos 配置静态 ip 时用的是脚本
                # 好像因此不起作用
                cat <>$conf_file
networking.dhcpcd.extraConfig =
  ''
    interface $ethx
      ipv6ra_noautoconf
  '';
EOF
                ;;
            3)
                # 暂时没用到 networkd
                cat <>$conf_file
systemd.network.networks.$ethx = {
   matchConfig.Name = "$ethx";
   networkConfig = {
     IPv6AcceptRA = false;
   };
 };
EOF
                ;;
            esac
        fi

        if should_disable_autoconf; then
            case "$mode" in
            1)
                cat <>$conf_file
boot.kernel.sysctl."net.ipv6.conf.$ethx.autoconf" = false;
EOF
                ;;
            2) ;;
            3) ;;
            esac
        fi
    done
}

install_alpine() {
    info "install alpine"

    need_ram=512
    swap_size=$(get_need_swap_size $need_ram)
    [ "$swap_size" -gt 0 ] && hack_lowram=true || hack_lowram=false

    # alpine 安装时会自动检测安装需要的 firmware
    # https://github.com/alpinelinux/alpine-conf/blob/3.18.1/setup-disk.in#L421
    # 但如果没有 modloop 则无法检测
    # 所以删除 modloop 前先记录用到的 firmware 包
    fw_pkgs=$(get_alpine_firmware_pkgs)

    if $hack_lowram; then
        # 预先加载需要的模块
        if rc-service -q modloop status; then
            modules="ext4 vfat nls_utf8 nls_cp437"
            for mod in $modules; do
                modprobe $mod
            done
            # crc32c 等于 crc32c-intel
            # 没有 sse4.2 的机器加载 crc32c 时会报错 modprobe: ERROR: could not insert 'crc32c_intel': No such device
            modprobe crc32c || modprobe crc32c-generic
        fi

        # 删除 modloop ,释放内存
        ensure_service_stopped modloop
        rm -f /lib/modloop-lts /lib/modloop-virt
    fi

    # bios机器用 setup-disk 自动分区会有 boot 分区
    # 因此手动分区安装
    create_part
    mount_part_basic_layout /os /os/boot/efi

    # 创建 swap
    if $hack_lowram; then
        create_swap $swap_size /os/swapfile
    fi

    # 网络配置
    create_ifupdown_config /etc/network/interfaces
    echo
    cat -n /etc/network/interfaces
    echo

    # 在 arm netboot initramfs init 中
    # 如果识别到rtc硬件,就往系统添加hwclock服务,否则添加swclock
    # 这个设置也被复制到安装的系统中
    # 但是从initramfs chroot到真正的系统后,是能识别rtc硬件的
    # 所以我们手动改用hwclock修复这个问题
    rc-update del swclock boot || true
    rc-update add hwclock boot

    # 通过 setup-alpine 安装会启用以下几个服务
    # https://github.com/alpinelinux/alpine-conf/blob/3.18.1/setup-alpine.in#L229

    # boot
    rc-update add networking boot
    rc-update add seedrng boot

    # default
    rc-update add crond
    if [ -e /dev/input/event0 ]; then
        rc-update add acpid
    fi

    # 如果是 vm 就用 virt 内核
    if is_virt; then
        kernel_flavor="virt"
    else
        kernel_flavor="lts"
    fi

    # 重置为官方仓库配置
    # 国内机可能无法访问mirror列表而报错
    if false; then
        true >/etc/apk/repositories
        setup-apkrepos -1
    fi

    # setup-disk 安装 grub 跳过了添加引导项到 nvram
    # 防止部分机器不会 fallback 到 bootx64.efi
    if is_efi; then
        apk add efibootmgr
        sed -i 's/--no-nvram//' "$(which setup-disk)"
    fi

    # 安装到硬盘
    # alpine默认使用 syslinux (efi 环境除外),这里强制使用 grub,方便用脚本再次重装
    KERNELOPTS="$(get_ttys console=)"
    export KERNELOPTS
    export BOOTLOADER="grub"
    setup-disk -m sys -k $kernel_flavor /os

    # 删除 setup-disk 时自动安装的包
    apk del e2fsprogs dosfstools efibootmgr grub*

    # 如果没有挂载 /proc

    # 1. chroot /os setup-keymap us us 会报错
    # grep: /proc/filesystems: No such file or directory

    # 2. 安装固件微码会触发 grub-probe,如果没挂载会报错
    # Executing grub-2.12-r5.trigger
    # /usr/sbin/grub-probe: error: failed to get canonical path of `/dev/vda1'.
    # ERROR: grub-2.12-r5.trigger: script exited with error 1

    mount_pseudo_fs /os

    # 安装到硬盘后才安装各种应用
    # 避免占用 Live OS 内存

    # 网络
    # udhcpc
    # 坑1 ip -4 addr 无法知道是否是 dhcp
    # 坑2 networking 服务不会运行 udhcpc6
    # 坑3 h3c 移动云电脑 udhcpc6 无法获取 dhcpv6

    # dhcpcd
    # 坑1 slaac默认开了隐私保护,造成ip和后台面板不一致

    # slaac方案1: udhcpc + rdnssd
    # slaac方案2: dhcpcd + 关闭隐私保护
    # dhcpv6方案: dhcpcd

    # 综合使用dhcpcd方案
    # 1 无需改动/etc/network/interfaces,自动根据ra使用slaac和dhcpv6
    # 2 自带rdnss支持
    # 3 唯一要做的是关闭隐私保护

    # 安装 dhcpcd
    chroot /os apk add dhcpcd
    chroot /os sed -i '/^slaac private/s/^/#/' /etc/dhcpcd.conf
    chroot /os sed -i '/^#slaac hwaddr/s/^#//' /etc/dhcpcd.conf

    # 安装其他部件
    chroot /os setup-keymap us us
    chroot /os setup-timezone -i Asia/Shanghai
    # 3.21 默认是 chrony
    # 3.22 默认是 busybox ntp
    printf '\n' | chroot /os setup-ntp || true

    # 设置公钥
    if is_need_set_ssh_keys; then
        set_ssh_keys_and_del_password /os
    fi

    # 下载 fix-eth-name
    download "$confhome/fix-eth-name.sh" /os/fix-eth-name.sh
    download "$confhome/fix-eth-name.initd" /os/etc/init.d/fix-eth-name
    chmod +x /os/etc/init.d/fix-eth-name
    chroot /os rc-update add fix-eth-name boot

    # 安装 frpc
    if ls /configs/frpc.* >/dev/null 2>&1; then
        chroot /os apk add frp
        # chroot rc-update add 默认添加到 sysinit
        # 但不加 chroot 默认添加到 default
        chroot /os rc-update add frpc boot
        cp -f /configs/frpc.* /os/etc/frp/
    fi

    # setup-disk 会自动选择固件,但不包括微码?
    # https://github.com/alpinelinux/alpine-conf/blob/3.18.1/setup-disk.in#L421
    if fw_pkgs="$fw_pkgs $(get_ucode_firmware_pkgs)" && [ -n "$fw_pkgs" ]; then
        chroot /os apk add $fw_pkgs
    fi

    # 3.19 或以上,非 efi 需要手动安装 grub
    if ! is_efi; then
        chroot /os grub-install --target=i386-pc /dev/$xda
    fi

    # efi grub 添加 fwsetup 条目
    chroot /os update-grub

    # 是否保留 swap
    if [ -e /os/swapfile ]; then
        if false; then
            echo "/swapfile swap swap defaults 0 0" >>/os/etc/fstab
            ln -sf /etc/init.d/swap /os/etc/runlevels/boot/swap
        else
            swapoff -a
            rm /os/swapfile
        fi
    fi
}

get_cpu_vendor() {
    cpu_vendor=$(grep 'vendor_id' /proc/cpuinfo | head -1 | awk '{print $NF}')
    case "$cpu_vendor" in
    GenuineIntel) echo intel ;;
    AuthenticAMD) echo amd ;;
    *) echo other ;;
    esac
}

min() {
    printf "%d\n" "$@" | sort -n | head -n 1
}

# 设置线程
# 根据 cpu 核数,每个线程的内存,取最小值
get_build_threads() {
    threads_per_mb=$1

    threads_by_core=$(nproc)
    threads_by_ram=$(($(get_approximate_ram_size) / threads_per_mb))
    [ $threads_by_ram -eq 0 ] && threads_by_ram=1
    min $threads_by_ram $threads_by_core
}

add_newline() {
    # shellcheck disable=SC1003
    case "$1" in
    head | start) sed -e '1s/^/\n/' ;;
    tail | end) sed -e '$a\\' ;;
    both) sed -e '1s/^/\n/' -e '$a\\' ;;
    esac
}

install_nixos() {
    info "Install NixOS"

    os_dir=/os
    keep_swap=true
    nix_from=website
    ram_per_thread=2048

    threads=$(get_build_threads $ram_per_thread)
    swap_size=$(get_need_swap_size $ram_per_thread)

    show_nixos_config() {
        echo
        # 过滤 frp auth.token
        cat -n /os/etc/nixos/configuration.nix | grep -Fv 'auth.token'
        echo
        cat -n /os/etc/nixos/hardware-configuration.nix
        echo
    }

    # 挂载分区,创建 swapfile
    mount_part_basic_layout /os /os/efi
    if [ "$swap_size" -gt 0 ]; then
        create_swap "$swap_size" /os/swapfile
    fi

    # 步骤
    # 1. 安装 nix (nix-xxx)
    # 2. 用 nix 安装 nixos-install-tools (nixos-xxx)
    # 3. 运行 nixos-generate-config 生成配置 + 编辑
    # 4. 运行 nixos-install
    # https://nixos.org/manual/nixos/stable/index.html#sec-installing-from-other-distro

    # nix 安装方式                                    分支          版本
    # apk add nix                                    3.20         2.22.0  # nix 本体跟 alpine 正常的软件一样,不在 /nix/store 里面
    # env -iA nixpkgs.nix                            24.05        2.18.5
    # sh <(curl -L https://nixos.org/nix/install)   unstable?     2.24.2

    # apk add 安装的 nix 有时会卡在
    # copying path '/nix/store/gcbrjlfm5h21ybf1h2lfq773zafjmzjr-curl-8.7.1-man' from 'https://cache.nixos.org'...
    # 但是 cpu 空载

    # 安装 nix
    mkdir -p /os/nix /nix
    mount --bind /os/nix /nix

    # nix 安装脚本和 /root/.nix-profile/etc/profile.d/nix.sh 都会用到这两个变量
    # 但从 alpine local.d 运行没有这两个变量
    export USER=root
    export HOME=/root

    case "$nix_from" in
    alpine)
        apk add nix
        # 设置 nix 镜像和线程
        # alpine 默认设置了 4 线程
        # https://gitlab.alpinelinux.org/alpine/aports/-/blob/master/community/nix/APKBUILD#L125
        sed -i '/max-jobs/d' /etc/nix/nix.conf
        echo "max-jobs = $threads" >>/etc/nix/nix.conf
        if is_in_china; then
            echo "substituters = $mirror/store" >>/etc/nix/nix.conf
        fi
        rc-service -q nix-daemon restart
        # 添加 nix-env 安装的软件到 PATH
        PATH="/root/.nix-profile/bin:$PATH"
        ;;
    website)
        # https://gitlab.alpinelinux.org/alpine/aports/-/blob/master/community/nix/nix.pre-install
        # https://nix.dev/manual/nix/latest/installation/multi-user
        if ! grep -q nixbld /etc/passwd; then
            addgroup -S nixbld
            for n in $(seq 1 10); do
                adduser -S -D -H -h /var/empty -s /sbin/nologin -G nixbld \
                    -g "Nix build user $n" nixbld$n
            done
        fi

        # 备用方案
        # 1. 从 https://mirror.nju.edu.cn/nix-channels/nixos-25.11/nixexprs.tar.xz 获取
        #    https://github.com/NixOS/nixpkgs/blob/nixos-25.11/pkgs/tools/package-management/nix/default.nix
        #    https://github.com/NixOS/nixpkgs/blob/nixos-25.11/nixos/modules/installer/tools/nix-fallback-paths.nix
        # 2. 安装最新版 nix,添加 nixos channel 后获取
        #    nix eval -f '' --raw 'nixVersions.stable.version' --extra-experimental-features nix-command

        if true; then
            # nix 版本号使用目标系统里面的
            download $mirror/nixos-$releasever/store-paths.xz /os/store-paths.xz
            apk add xz
            nix_ver=$(xz -dc /dev/null 2>&1; then
        nix_frpc=$(
            if false; then
                # 原始 frpc.toml 转 toml 对象 ,再转成最终使用的 frpc.toml
                # 可以避免原始 frpc.toml 有错误导致失联
                # 但是 frpc 配置还支持 ini json yaml
                # 因此不使用这个方法
                cat <"$os_dir/usr/lib/systemd/system-preset/01-$service_name.preset"
    else
        echo "enable $service_name.service" >"$os_dir/lib/systemd/system-preset/01-$service_name.preset"
    fi
}

add_fix_eth_name_systemd_service() {
    local os_dir=$1

    # 无需执行 systemctl daemon-reload
    # 因为 chroot 下执行会提示 Running in chroot, ignoring command 'daemon-reload'
    download "$confhome/fix-eth-name.sh" "$os_dir/fix-eth-name.sh"
    add_systemd_service "$os_dir" fix-eth-name
}

get_frpc_url() {
    wget "$confhome/get-frpc-url.sh" -O- | sh -s "$@"
}

add_frpc_systemd_service_if_need() {
    local os_dir=$1

    if ls /configs/frpc.* >/dev/null 2>&1; then
        mkdir -p "$os_dir/usr/local/bin"
        mkdir -p "$os_dir/usr/local/etc/frpc"

        # 下载 frpc
        # 注意下载的 frpc owner 不是 root:root
        frpc_url=$(get_frpc_url linux)
        basename=$(echo "$frpc_url" | awk -F/ '{print $NF}' | sed 's/\.tar\.gz//')
        download "$frpc_url" "$os_dir/frpc.tar.gz"
        # busybox tar 不支持 wildcard
        # tar: */frpc: not found in archive
        tar xzf "$os_dir/frpc.tar.gz" "$basename/frpc" -O >"$os_dir/usr/local/bin/frpc"
        rm -f "$os_dir/frpc.tar.gz"
        chmod a+x "$os_dir/usr/local/bin/frpc"

        # frpc conf
        cp -f /configs/frpc.* "$os_dir/usr/local/etc/frpc/"

        # 添加服务
        add_systemd_service "$os_dir" frpc
    fi
}

basic_init() {
    os_dir=$1

    # 此时不能用
    # chroot $os_dir timedatectl set-timezone Asia/Shanghai
    # Failed to create bus connection: No such file or directory

    # debian 11 没有 systemd-firstboot
    if is_have_cmd_on_disk $os_dir systemd-firstboot; then
        if chroot $os_dir systemd-firstboot --help | grep -wq '\--force'; then
            chroot $os_dir systemd-firstboot --timezone=Asia/Shanghai --force
        else
            chroot $os_dir systemd-firstboot --timezone=Asia/Shanghai
        fi
    fi

    # gentoo 不会自动创建 machine-id
    clear_machine_id $os_dir

    # sshd
    chroot $os_dir ssh-keygen -A

    sshd_enabled=false
    sshs="sshd.service ssh.service sshd.socket ssh.socket"
    for i in $sshs; do
        if chroot $os_dir systemctl -q is-enabled $i; then
            sshd_enabled=true
            break
        fi
    done
    if ! $sshd_enabled; then
        for i in $sshs; do
            if chroot $os_dir systemctl -q enable $i; then
                break
            fi
        done
    fi

    if is_need_change_ssh_port; then
        change_ssh_port $os_dir $ssh_port
    fi

    # 公钥/密码
    if is_need_set_ssh_keys; then
        set_ssh_keys_and_del_password $os_dir
    else
        change_root_password $os_dir
        allow_root_password_login $os_dir
        allow_password_login $os_dir
    fi

    # 下载 fix-eth-name.service
    # 即使开了 net.ifnames=0 也需要
    # 因为 alpine live 和目标系统的网卡顺序可能不同
    add_fix_eth_name_systemd_service $os_dir

    # frpc
    add_frpc_systemd_service_if_need $os_dir
}

install_arch_gentoo_aosc() {
    info "install $distro"

    network_app=$(
        case "$distro" in
        arch | gentoo) echo systemd-networkd ;;
        aosc) echo network-manager ;;
        esac
    )

    set_locale() {
        echo "C.UTF-8 UTF-8" >>$os_dir/etc/locale.gen
        chroot $os_dir locale-gen
    }

    # shellcheck disable=SC2317
    install_arch() {
        # 添加 swap
        create_swap_if_ram_less_than 1024 $os_dir/swapfile

        apk add arch-install-scripts

        # 为了二次运行时 /etc/pacman.conf 未修改
        if [ -f /etc/pacman.conf.orig ]; then
            cp /etc/pacman.conf.orig /etc/pacman.conf
        else
            cp /etc/pacman.conf /etc/pacman.conf.orig
        fi

        # 设置 repo
        insert_into_file /etc/pacman.conf before '\[core\]' <>/etc/pacman.conf
[core]
Include = /etc/pacman.d/mirrorlist

[extra]
Include = /etc/pacman.d/mirrorlist
EOF
        mkdir -p /etc/pacman.d
        # shellcheck disable=SC2016
        case "$(uname -m)" in
        x86_64) dir='$repo/os/$arch' ;;
        aarch64) dir='$arch/$repo' ;;
        esac
        # shellcheck disable=SC2154
        echo "Server = $mirror/$dir" >/etc/pacman.d/mirrorlist

        # 安装系统
        # 要安装分区工具(包含 fsck.xxx),用于 initramfs 检查分区数据
        # base 包含 e2fsprogs
        pkgs="base grub openssh"
        if is_efi; then
            pkgs="$pkgs efibootmgr dosfstools"
        fi
        if [ "$(uname -m)" = aarch64 ]; then
            pkgs="$pkgs archlinuxarm-keyring"
        fi
        pacstrap -K $os_dir $pkgs

        # dns
        cp_resolv_conf $os_dir

        # 挂载伪文件系统
        mount_pseudo_fs $os_dir

        # 要先设置语言,再安装内核,不然出现
        # ==> Creating gzip-compressed initcpio image: '/boot/initramfs-linux.img'
        # bsdtar: bsdtar: Failed to set default locale
        # Failed to set default locale
        set_locale
        if [ "$(uname -m)" = aarch64 ]; then
            chroot $os_dir pacman-key --lsign-key builder@archlinuxarm.org
        fi

        # firmware + microcode
        if fw_pkgs=$(get_ucode_firmware_pkgs) && [ -n "$fw_pkgs" ]; then
            chroot $os_dir pacman -Syu --noconfirm $fw_pkgs
        fi

        # arm 的内核有多种选择,默认是 linux-aarch64,所以要添加 --noconfirm
        chroot $os_dir pacman -Syu --noconfirm linux
    }

    # shellcheck disable=SC2317
    install_gentoo() {
        # 添加 swap
        create_swap_if_ram_less_than 2048 $os_dir/swapfile

        # 解压系统
        apk add tar xz pv
        # shellcheck disable=SC2154
        download "$img" $os_dir/gentoo.tar.xz
        echo "Uncompressing Gentoo..."
        pv -f $os_dir/gentoo.tar.xz | tar xpJ --numeric-owner --xattrs-include='*.*' -C $os_dir
        rm $os_dir/gentoo.tar.xz
        apk del tar xz pv

        # dns
        cp_resolv_conf $os_dir

        # 挂载伪文件系统
        mount_pseudo_fs $os_dir

        # 下载仓库,选择 profile
        chroot $os_dir emerge-webrsync
        profile=$(
            # 筛选 stable systemd,再选择最短的
            if false; then
                chroot $os_dir eselect profile list | grep stable | grep systemd |
                    awk '(NR == 1 || length($2) < length(shortest)) { shortest = $2 } END { print shortest }'
            else
                chroot $os_dir eselect profile list | grep stable | grep systemd |
                    awk '{print length($2), $2}' | sort -n | head -1 | awk '{print $2}'
            fi
        )
        echo "Select profile: $profile"
        chroot $os_dir eselect profile set $profile

        # 设置 license
        cat <>$os_dir/etc/portage/make.conf
ACCEPT_LICENSE="*"
EOF

        cat <>$os_dir/etc/portage/make.conf
MAKEOPTS="-j$(get_build_threads 2048)"
EOF

        # 设置 http repo + binpkg repo
        # https://mirror.nju.edu.cn/gentoo/releases/amd64/autobuilds/current-stage3-amd64-systemd-mergedusr/stage3-amd64-systemd-mergedusr-20240317T170433Z.tar.xz
        mirror_short=$(echo "$img" | sed 's,/releases/.*,,')
        mirror_long=$(echo "$img" | sed 's,/autobuilds/.*,,')
        profile_ver=$(chroot $os_dir eselect profile show | grep -Eo '/[0-9.]*/' | cut -d/ -f2)

        if [ "$(uname -m)" = x86_64 ]; then
            if chroot $os_dir ld.so --help | grep supported | grep -q x86-64-v3; then
                binpkg_type=x86-64-v3
            else
                binpkg_type=x86-64
            fi
        else
            binpkg_type=arm64
        fi

        cat <>$os_dir/etc/portage/make.conf
GENTOO_MIRRORS="$mirror_short"
FEATURES="getbinpkg"
EOF

        cat <$os_dir/etc/portage/binrepos.conf/gentoobinhost.conf
[binhost]
priority = 9999
sync-uri = $mirror_long/binpackages/$profile_ver/$binpkg_type
EOF

        # 下载公钥

        # getuto 会判断是否有 ${TERM} 且 ${TERM} 不是 dumb
        # 符合条件则 source /lib/gentoo/functions.sh 导入 ebegin 等方法
        # 不符合条件则自行创建 ebegin 等方法

        # /lib/gentoo/functions.sh 会判断是否有 ${RC_OPENRC_PID}
        # 有的话就不会导入 /functions/openrc.sh,也就不会导入 ebegin 方法

        # 在 ssh 里运行 /trans.sh 时,没有 ${RC_OPENRC_PID},${TERM} 是 xterm
        # 因此 chroot $os_dir getuto 时不会报错

        # 在 locald 里运行 /trans.sh 时,有 ${RC_OPENRC_PID},${TERM} 是 linux
        # 因此 chroot $os_dir getuto 时会报错说 ebegin: command not found

        # 有两个解决方法
        if true; then
            TERM=dumb chroot $os_dir getuto
        else
            env -u RC_OPENRC_PID chroot $os_dir getuto
        fi

        set_locale

        # 安装 git 会升级 glibc,此时 /etc/locale.gen 不能为空,否则会提示生成所有 locale
        # Generating all locales; edit /etc/locale.gen to save time/space
        chroot $os_dir emerge dev-vcs/git

        # 设置 git repo
        if is_in_china; then
            git_uri=https://mirror.nju.edu.cn/git/gentoo-portage.git
        else
            # github 不支持 ipv6
            is_any_ipv4_has_internet && git_uri=https://github.com/gentoo-mirror/gentoo.git ||
                git_uri=https://anongit.gentoo.org/git/repo/gentoo.git
        fi

        mkdir -p $os_dir/etc/portage/repos.conf
        cat <$os_dir/etc/portage/repos.conf/gentoo.conf
[gentoo]
location = /var/db/repos/gentoo
sync-type = git
sync-uri = $git_uri
EOF
        rm -rf $os_dir/var/db/repos/gentoo
        chroot $os_dir emerge --sync

        if [ "$(uname -m)" = x86_64 ]; then
            # https://packages.gentoo.org/packages/sys-block/io-scheduler-udev-rules
            chroot $os_dir emerge sys-block/io-scheduler-udev-rules
        fi

        if is_efi; then
            chroot $os_dir emerge sys-fs/dosfstools
        fi

        # firmware + microcode
        if fw_pkgs=$(get_ucode_firmware_pkgs) && [ -n "$fw_pkgs" ]; then
            chroot $os_dir emerge $fw_pkgs
        fi

        # 安装 grub + 内核
        # TODO: 先判断是否有 binpkg,有的话不修改 GRUB_PLATFORMS
        is_efi && grub_platforms="efi-64" || grub_platforms="pc"
        echo GRUB_PLATFORMS=\"$grub_platforms\" >>$os_dir/etc/portage/make.conf
        echo "sys-kernel/installkernel dracut grub" >$os_dir/etc/portage/package.use/installkernel
        chroot $os_dir emerge sys-kernel/gentoo-kernel-bin
    }

    install_aosc() {
        # 解压系统
        apk add wget tar xz
        wget "$img" -O- | tar xpJ --numeric-owner --xattrs-include='*.*' -C $os_dir
        apk del wget tar xz

        # 添加 swap
        create_swap_if_ram_less_than 1024 $os_dir/swapfile

        # 挂载伪文件系统
        mount_pseudo_fs $os_dir

        # 生成 initramfs
        chroot $os_dir update-initramfs
    }

    os_dir=/os

    # 挂载分区
    mount_part_basic_layout /os /os/efi

    # 安装系统
    install_$distro

    # 安装 arch 有 gpg-agent 进程驻留
    pkill gpg-agent || true

    # 初始化
    if false; then
        # preset-all 后多了很多服务,内存占用多了几十M
        chroot $os_dir systemctl preset-all
    fi

    # 网络配置
    case "$network_app" in
    systemd-networkd)
        chroot $os_dir systemctl enable systemd-networkd
        chroot $os_dir systemctl enable systemd-resolved

        apk add cloud-init
        # 第二次运行会报错
        useradd systemd-network || true
        create_cloud_init_network_config net.cfg
        cat -n net.cfg
        # 正常应该是 -D gentoo,但 alpine 的 cloud-init 包缺少 gentoo 配置
        cloud-init devel net-convert -p net.cfg -k yaml -d out -D alpine -O networkd

        # 注意名字是 10-cloud-init-eth*.network,fix-eth-name.sh 会此文件名查找配置文件
        cp out/etc/systemd/network/10-cloud-init-eth*.network $os_dir/etc/systemd/network/

        # 删除网卡名匹配
        sed -i '/^Name=/d' $os_dir/etc/systemd/network/10-cloud-init-eth*.network

        # 删除 Generated by cloud-init. Changes will be lost.
        # 并删除头部的空行
        sed -i '/^# Generated by cloud-init/d' $os_dir/etc/systemd/network/10-cloud-init-eth*.network
        del_head_empty_lines_inplace $os_dir/etc/systemd/network/10-cloud-init-eth*.network

        # 清理
        rm -rf net.cfg out
        apk del cloud-init

        # 显示网络配置
        cat -n $os_dir/etc/systemd/network/10-cloud-init-eth*.network
        ;;
    network-manager)
        chroot $os_dir systemctl enable NetworkManager

        # 可以直接用 alpine 的 cloud-init 生成 Network Manager 配置
        create_cloud_init_network_config /net.cfg
        create_network_manager_config /net.cfg "$os_dir"
        rm /net.cfg
        ;;
    esac

    # arch gentoo 网络配置是用 alpine cloud-init 生成的
    # cloud-init 版本够新,因此无需修复 onlink 网关

    basic_init $os_dir

    # ntp 用 systemd 自带的
    # TODO: vm agent + 随机数生成器

    # grub
    if is_efi; then
        # arch gentoo 推荐 efi 挂载在 /efi
        chroot $os_dir grub-install --efi-directory=/efi
        chroot $os_dir grub-install --efi-directory=/efi --removable
    else
        chroot $os_dir grub-install /dev/$xda
    fi

    # cmdline + 生成 grub.cfg
    if [ -d $os_dir/etc/default/grub.d ]; then
        file=$os_dir/etc/default/grub.d/tty.cfg
    else
        file=$os_dir/etc/default/grub
    fi
    ttys_cmdline=$(get_ttys console=)
    echo GRUB_CMDLINE_LINUX=\"\$GRUB_CMDLINE_LINUX $ttys_cmdline\" >>$file
    chroot $os_dir grub-mkconfig -o /boot/grub/grub.cfg

    # fstab
    # fstab 可不写 efi 条目, systemd automount 会自动挂载
    # fstab 头部有使用说明,因此用 >>
    apk add arch-install-scripts
    genfstab -U $os_dir | sed '/swap/d' >>$os_dir/etc/fstab
    apk del arch-install-scripts

    # 删除 resolv.conf,不然 systemd-resolved 无法创建软链接
    rm_resolv_conf $os_dir

    # 删除 swap
    swapoff -a
    rm -rf $os_dir/swapfile
}

get_http_file_size() {
    url=$1

    # 网址重定向可能得到多个 Content-Length, 选最后一个
    wget --spider -S "$url" 2>&1 | grep 'Content-Length:' |
        tail -1 | awk '{print $2}' | grep .
}

get_url_hash() {
    url=$1

    echo "$url" | md5sum | awk '{print $1}'
}

aria2c() {
    if ! is_have_cmd aria2c; then
        apk add aria2
    fi

    # stdbuf 在 coreutils 包里面
    if ! is_have_cmd stdbuf; then
        apk add coreutils
    fi

    # 显示 url
    show_url_in_args "$@" >&2

    # 下载 tracker
    # 在 sub shell 里面无法保存变量,因此写入到文件
    if echo "$@" | grep -Eq 'magnet:|\.torrent' && ! [ -f "/tmp/trackers" ]; then
        # 独自一行下载,不然下载失败不会报错
        # 里面有空行
        # txt=$(wget -O- https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt | grep .)
        # txt=$(wget -O- https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt | grep .)
        txt=$(wget -O- https://cf.trackerslist.com/best.txt | grep .)
        # sed 删除最后一个逗号
        echo "$txt" | newline_to_comma | sed 's/,$//' >/tmp/trackers
    fi

    # --dht-entry-point=router.bittorrent.com:6881 \
    # --dht-entry-point=dht.transmissionbt.com:6881 \
    # --dht-entry-point=router.utorrent.com:6881 \
    retry 5 5 stdbuf -oL -eL aria2c \
        -x4 \
        --seed-time=0 \
        --allow-overwrite=true \
        --summary-interval=0 \
        --max-tries 1 \
        --bt-tracker="$([ -f "/tmp/trackers" ] && cat /tmp/trackers)" \
        "$@"
}

download_torrent_by_magnet() {
    url=$1
    dst=$2

    url_hash=$(get_url_hash "$url")

    mkdir -p /tmp/bt/$url_hash

    # 不支持 -o bt.torrent 指定文件名
    aria2c "$url" \
        --bt-metadata-only=true \
        --bt-save-metadata=true \
        -d /tmp/bt/$url_hash

    mv /tmp/bt/$url_hash/*.torrent "$dst"
    rm -rf /tmp/bt/$url_hash
}

get_torrent_path_by_magnet() {
    echo "/tmp/bt/$(get_url_hash "$1").torrent"
}

get_bt_file_size() {
    url=$1

    torrent="$(get_torrent_path_by_magnet $url)"
    download_torrent_by_magnet "$url" "$torrent" >&2

    # 列出第一个文件的大小
    # idx|path/length
    # ===+===========================================================================
    #   1|./zh-cn_windows_11_consumer_editions_version_24h2_updated_jan_2025_x64_dvd_7a8e5a29.iso
    #    |6.1GiB (6,557,558,784)

    aria2c --show-files=true "$torrent" |
        grep -F -A1 '  1|./' | tail -1 | grep -o '(.*)' | sed -E 's/[(),]//g' | grep .
}

get_link_file_size() {
    if is_magnet_link "$1" >&2; then
        get_bt_file_size "$1"
    else
        get_http_file_size "$1"
    fi
}

pipe_extract() {
    # alpine busybox 自带 gzip,但官方版也许性能更好
    case "$img_type_warp" in
    xz | gzip | zstd)
        apk add $img_type_warp
        "$img_type_warp" -dc
        ;;
    tar)
        apk add tar
        tar x -O
        ;;
    tar.*)
        type=$(echo "$img_type_warp" | cut -d. -f2)
        apk add tar "$type"
        tar x "--$type" -O
        ;;
    '') cat ;;
    *) error_and_exit "Not supported img_type_warp: $img_type_warp" ;;
    esac
}

dd_raw_with_extract() {
    info "dd raw"

    # 用官方 wget,一来带进度条,二来自带重试功能
    apk add wget

    if ! wget $img -O- | pipe_extract >/dev/$xda 2>/tmp/dd_stderr; then
        # vhd 文件结尾有 512 字节额外信息,可以忽略
        if grep -iq 'No space' /tmp/dd_stderr; then
            apk add parted
            disk_size=$(get_disk_size /dev/$xda)
            disk_end=$((disk_size - 1))

            # 如果报错,那大概是因为镜像比硬盘大
            if last_part_end=$(parted -sf /dev/$xda 'unit b print' ---pretend-input-tty |
                del_empty_lines | tail -1 | awk '{print $3}' | sed 's/B//' | grep .); then

                echo "Last part end: $last_part_end"
                echo "Disk end:      $disk_end"

                if [ "$last_part_end" -le "$disk_end" ]; then
                    echo "Safely ignore no space error."
                    return
                fi
            fi
        fi
        error_and_exit "$(cat /tmp/dd_stderr)"
    fi
}

get_disk_sector_count() {
    # cat /proc/partitions
    blockdev --getsz "$1"
}

get_disk_size() {
    blockdev --getsize64 "$1"
}

get_disk_logic_sector_size() {
    blockdev --getss "$1"
}

is_4kn() {
    [ "$(blockdev --getss "/dev/$xda")" = 4096 ]
}

is_xda_gt_2t() {
    disk_size=$(get_disk_size /dev/$xda)
    disk_2t=$((2 * 1024 * 1024 * 1024 * 1024))
    [ "$disk_size" -gt "$disk_2t" ]
}

create_part() {
    # 除了 dd 都会用到
    info "Create Part"

    # 分区工具
    apk add parted e2fsprogs
    if is_efi; then
        apk add dosfstools
    fi

    # 清除分区签名
    # TODO: 先检测iso链接/各种链接
    # wipefs -a /dev/$xda

    # xda*1 星号用于 nvme0n1p1 的字母 p
    # shellcheck disable=SC2154
    if [ "$distro" = windows ]; then
        if ! size_bytes=$(get_link_file_size "$iso"); then
            # 默认值,目前最大的 iso 小于 8g
            size_bytes=$((8 * 1024 * 1024 * 1024))
        fi

        # 按iso容量计算分区大小
        # 200m 用于驱动/文件系统自身占用 + pagefile
        # 理论上 installer 分区可以删除 boot.wim,这样就不用额外添加 200m,但是
        # 1. vista/2008 不能删除 boot.wim
        # 2. 下载镜像前不知道是 vista/2008,因为 --image-name 可以随便输入
        # 因此还是要额外添加 200m
        # 注意这里单位要用 MiB,因为后面的 border 要以 MiB 计算
        part_size="$((size_bytes / 1024 / 1024 + 200))MiB"

        apk add ntfs-3g-progs
        # 虽然ntfs3不需要fuse,但wimmount需要,所以还是要保留
        modprobe fuse ntfs3
        if is_efi; then
            # efi
            parted /dev/$xda -s -- \
                mklabel gpt \
                mkpart '" "' fat32 1MiB 1025MiB \
                mkpart '" "' fat32 1025MiB 1041MiB \
                mkpart '" "' ntfs 1041MiB -${part_size} \
                mkpart '" "' ntfs -${part_size} 100% \
                set 1 boot on \
                set 2 msftres on \
                set 3 msftdata on
            update_part

            mkfs.fat -n efi /dev/$xda*1                           #1 efi
            dd if=/dev/zero of="$(ls /dev/$xda*2)" bs=1M count=16 #2 msr
            mkfs.ntfs -f -F -L os /dev/$xda*3                     #3 os
            mkfs.ntfs -f -F -L installer /dev/$xda*4              #4 installer
        else
            # bios + mbr 启动盘最大可用 2t
            if is_xda_gt_2t; then
                border=$((2 * 1024 * 1024 - ${part_size%MiB}))MiB
                max_usable_size=2TiB
            else
                border=-${part_size}
                max_usable_size=100%
            fi
            parted /dev/$xda -s -- \
                mklabel msdos \
                mkpart primary ntfs 1MiB ${border} \
                mkpart primary ntfs ${border} ${max_usable_size} \
                set 1 boot on
            update_part

            mkfs.ntfs -f -F -L os /dev/$xda*1        #1 os
            mkfs.ntfs -f -F -L installer /dev/$xda*2 #2 installer
        fi
    elif [ "$distro" = fnos ]; then
        # 先用 100% 分区安装后再缩小没意义,因为小硬盘用 100% 还是装不了
        # 因此直接用用户输入的分区大小

        # 1. 官方安装器对系统盘大小的定义包含引导分区大小
        # 2. 官方 efi 用的是 1MiB-100M,但我们用 1MiB-101MiB

        # 预期的系统分区大小,包括引导的 1M + 100M 的引导分区
        expect_m=$((${fnos_part_size%[Gg]} * 1024))

        sector_size=$(get_disk_logic_sector_size /dev/$xda)
        total_sector_count=$(get_disk_sector_count /dev/$xda)

        # 截止最后一个分区的总扇区数(也就是总硬盘扇区数 - 备份分区表扇区数 - 备份 GPT Header)
        if ! is_efi && ! is_xda_gt_2t; then
            # mbr
            total_sector_count_except_backup_gpt=$total_sector_count
        elif is_4kn; then
            total_sector_count_except_backup_gpt=$((total_sector_count - 4 - 1))
        else
            total_sector_count_except_backup_gpt=$((total_sector_count - 32 - 1))
        fi

        # 向下取整 MiB
        # gpt 最后 33 (512n/512e) 或 5 (4Kn) 个扇区是备份分区表,不可用
        # parted 结束位置填 100% 时也会忽略最后不足 1MiB 的部分,我们模仿它
        max_can_use_m=$((total_sector_count_except_backup_gpt * sector_size / 1024 / 1024))

        echo "expect_m: $expect_m"
        echo "max_can_use_m: $max_can_use_m"

        # 20G 的硬盘,即使用 msdos 分区表,parted 也不接受 part end 为 20480MiB,因此要用 100%
        # The location 20480MiB is outside of the device /dev/vda.
        # 但是 100% 分区后 end 就是 20480MiB

        os_part_end=${expect_m}MiB
        if [ "$expect_m" -ge "$max_can_use_m" ]; then
            echo "Expect size is equal/greater than max size. Setting to 100%"
            os_part_end=100%
        fi

        # 需关闭这几个特性,否则 grub 无法识别
        ext4_opts="-O ^metadata_csum_seed,^orphan_file"
        if is_efi; then
            parted /dev/$xda -s -- \
                mklabel gpt \
                mkpart BOOT fat32 1MiB 101MiB \
                mkpart SYSTEM ext4 101MiB $os_part_end \
                set 1 esp on
            update_part

            mkfs.fat /dev/$xda*1                #1 efi
            mkfs.ext4 -F $ext4_opts /dev/$xda*2 #2 os + installer
        elif is_xda_gt_2t; then
            # bios > 2t
            # 官方安装器是 mkpart BOOT 1M 100M,无论 esp 或者 bios_grub 都用这个分区和大小
            parted /dev/$xda -s -- \
                mklabel gpt \
                mkpart BOOT ext4 1MiB 101MiB \
                mkpart SYSTEM ext4 101MiB $os_part_end \
                set 1 bios_grub on
            update_part

            echo                                #1 bios_boot
            mkfs.ext4 -F $ext4_opts /dev/$xda*2 #2 os + installer
        else
            # bios
            parted /dev/$xda -s -- \
                mklabel msdos \
                mkpart primary 1MiB 101MiB \
                mkpart primary 101MiB $os_part_end \
                set 2 boot on
            update_part

            echo                                #1 官方安装有这个分区
            mkfs.ext4 -F $ext4_opts /dev/$xda*2 #2 os + installer
        fi
    elif is_use_cloud_image; then
        installer_part_size="$(get_cloud_image_part_size)"
        # 这几个系统不使用dd,而是复制文件
        if [ "$distro" = centos ] || [ "$distro" = almalinux ] || [ "$distro" = rocky ] ||
            [ "$distro" = oracle ] || [ "$distro" = redhat ] ||
            [ "$distro" = anolis ] || [ "$distro" = opencloudos ] || [ "$distro" = openeuler ] ||
            [ "$distro" = ubuntu ]; then
            # 这里的 fs 没有用,最终使用目标系统的格式化工具
            fs=ext4
            if is_efi; then
                parted /dev/$xda -s -- \
                    mklabel gpt \
                    mkpart '" "' fat32 1MiB 101MiB \
                    mkpart '" "' $fs 101MiB -$installer_part_size \
                    mkpart '" "' ext4 -$installer_part_size 100% \
                    set 1 esp on
                update_part

                mkfs.fat -n efi /dev/$xda*1           #1 efi
                echo                                  #2 os 用目标系统的格式化工具
                mkfs.ext4 -F -L installer /dev/$xda*3 #3 installer
            else
                parted /dev/$xda -s -- \
                    mklabel gpt \
                    mkpart '" "' ext4 1MiB 2MiB \
                    mkpart '" "' $fs 2MiB -$installer_part_size \
                    mkpart '" "' ext4 -$installer_part_size 100% \
                    set 1 bios_grub on
                update_part

                echo                                  #1 bios_boot
                echo                                  #2 os 用目标系统的格式化工具
                mkfs.ext4 -F -L installer /dev/$xda*3 #3 installer
            fi
        else
            # 使用 dd qcow2
            # fedora debian opensuse arch gentoo
            parted /dev/$xda -s -- \
                mklabel gpt \
                mkpart '" "' ext4 1MiB -$installer_part_size \
                mkpart '" "' ext4 -$installer_part_size 100%
            update_part

            mkfs.ext4 -F -L os /dev/$xda*1        #1 os
            mkfs.ext4 -F -L installer /dev/$xda*2 #2 installer
        fi
    elif [ "$distro" = alpine ] || [ "$distro" = arch ] || [ "$distro" = gentoo ] ||
        [ "$distro" = nixos ] || [ "$distro" = aosc ]; then
        # alpine 本身关闭了 64bit ext4
        # https://gitlab.alpinelinux.org/alpine/alpine-conf/-/blob/3.18.1/setup-disk.in?ref_type=tags#L908
        # 而且 alpine 的 extlinux 不兼容 64bit ext4
        [ "$distro" = alpine ] && ext4_opts="-O ^64bit" || ext4_opts=
        if is_efi; then
            # efi
            parted /dev/$xda -s -- \
                mklabel gpt \
                mkpart '" "' fat32 1MiB 101MiB \
                mkpart '" "' ext4 101MiB 100% \
                set 1 boot on
            update_part

            mkfs.fat /dev/$xda*1                #1 efi
            mkfs.ext4 -F $ext4_opts /dev/$xda*2 #2 os
        elif is_xda_gt_2t; then
            # bios > 2t
            parted /dev/$xda -s -- \
                mklabel gpt \
                mkpart '" "' ext4 1MiB 2MiB \
                mkpart '" "' ext4 2MiB 100% \
                set 1 bios_grub on
            update_part

            echo                                #1 bios_boot
            mkfs.ext4 -F $ext4_opts /dev/$xda*2 #2 os
        else
            # bios
            parted /dev/$xda -s -- \
                mklabel msdos \
                mkpart primary ext4 1MiB 100% \
                set 1 boot on
            update_part

            mkfs.ext4 -F $ext4_opts /dev/$xda*1 #1 os
        fi
    else
        # 安装红帽系或ubuntu
        # 对于红帽系是临时分区表,安装时除了 installer 分区,其他分区会重建为默认的大小
        # 对于ubuntu是最终分区表,因为 ubuntu 的安装器不能调整个别分区,只能重建整个分区表
        # installer 2g分区用fat格式刚好塞得下ubuntu-22.04.3 iso,而ext4塞不下或者需要改参数
        if [ "$distro" = ubuntu ]; then
            if ! size_bytes=$(get_http_file_size "$iso"); then
                # 默认值,假设 iso 3g
                size_bytes=$((3 * 1024 * 1024 * 1024))
            fi
            installer_part_size="$(get_part_size_mb_for_file_size_b $size_bytes)MiB"
        else
            # redhat
            installer_part_size=2GiB
        fi

        # centos 7 无法加载alpine格式化的ext4
        # 要关闭这个属性
        ext4_opts="-O ^metadata_csum"
        apk add dosfstools

        if is_efi; then
            # efi
            parted /dev/$xda -s -- \
                mklabel gpt \
                mkpart '" "' fat32 1MiB 1025MiB \
                mkpart '" "' ext4 1025MiB -$installer_part_size \
                mkpart '" "' ext4 -$installer_part_size 100% \
                set 1 boot on
            update_part

            mkfs.fat -n efi /dev/$xda*1                      #1 efi
            mkfs.ext4 -F -L os /dev/$xda*2                   #2 os
            mkfs.ext4 -F -L installer $ext4_opts /dev/$xda*3 #2 installer
        elif is_xda_gt_2t; then
            # bios > 2t
            parted /dev/$xda -s -- \
                mklabel gpt \
                mkpart '" "' ext4 1MiB 2MiB \
                mkpart '" "' ext4 2MiB -$installer_part_size \
                mkpart '" "' ext4 -$installer_part_size 100% \
                set 1 bios_grub on
            update_part

            echo                                             #1 bios_boot
            mkfs.ext4 -F -L os /dev/$xda*2                   #2 os
            mkfs.ext4 -F -L installer $ext4_opts /dev/$xda*3 #3 installer
        else
            # bios
            parted /dev/$xda -s -- \
                mklabel msdos \
                mkpart primary ext4 1MiB -$installer_part_size \
                mkpart primary ext4 -$installer_part_size 100% \
                set 1 boot on
            update_part

            mkfs.ext4 -F -L os /dev/$xda*1                   #1 os
            mkfs.ext4 -F -L installer $ext4_opts /dev/$xda*2 #2 installer
        fi
        update_part
    fi

    update_part

    # alpine 删除分区工具,防止 256M 小机爆内存
    # setup-disk /dev/sda 会保留格式化工具,我们也保留
    if [ "$distro" = alpine ]; then
        apk del parted
    fi
}

umount_pseudo_fs() {
    os_dir=$(realpath "$1")

    dirs="/proc /sys /dev /run"
    regex=$(echo "$dirs" | sed 's, ,|,g')
    if mounts=$(mount | grep -Ew "on $os_dir($regex)" | awk '{print $3}' | tac); then
        for mount in $mounts; do
            echo "umount $mount"
            umount $mount
        done
    fi
}

mount_pseudo_fs() {
    os_dir=$1

    mkdir -p $os_dir/proc/ $os_dir/sys/ $os_dir/dev/ $os_dir/run/

    # https://wiki.archlinux.org/title/Chroot#Using_chroot
    mount -t proc /proc $os_dir/proc/
    mount -t sysfs /sys $os_dir/sys/
    mount --rbind /dev $os_dir/dev/
    mount --rbind /run $os_dir/run/
    if is_efi; then
        mount --rbind /sys/firmware/efi/efivars $os_dir/sys/firmware/efi/efivars/
    fi
}

create_cloud_init_network_config() {
    ci_file=$1
    recognize_static6=${2:-true}
    recognize_ipv6_types=${3:-true}

    info "Create Cloud-init network config"

    # 防止文件未创建
    mkdir -p "$(dirname "$ci_file")"
    touch "$ci_file"

    apk add yq-go

    need_set_dns4=false
    need_set_dns6=false

    config_id=0
    for ethx in $(get_eths); do
        get_netconf_to mac_addr

        # shellcheck disable=SC2154
        yq -i ".network.version=1 |
           .network.config[$config_id].type=\"physical\" |
           .network.config[$config_id].name=\"$ethx\" |
           .network.config[$config_id].mac_address=(\"$mac_addr\" | . style=\"single\")
           " $ci_file

        subnet_id=0

        # ipv4
        if is_dhcpv4; then
            yq -i ".network.config[$config_id].subnets[$subnet_id] = {\"type\": \"dhcp4\"}" $ci_file
            subnet_id=$((subnet_id + 1))
        elif is_staticv4; then
            need_set_dns4=true
            get_netconf_to ipv4_addr
            get_netconf_to ipv4_gateway
            yq -i ".network.config[$config_id].subnets[$subnet_id] = {
                    \"type\": \"static\",
                    \"address\": \"$ipv4_addr\",
                    \"gateway\": \"$ipv4_gateway\" }
                    " $ci_file

            # 旧版 cloud-init 有 bug
            # 有的版本会只从第一种配置中读取 dns,有的从第二种读取
            # 因此写两种配置
            # https://github.com/canonical/cloud-init/commit/1b8030e0c7fd6fbff7e38ad1e3e6266ae50c83a5
            for cur in $(get_current_dns 4); do
                yq -i ".network.config[$config_id].subnets[$subnet_id].dns_nameservers += [\"$cur\"]" $ci_file
            done
            subnet_id=$((subnet_id + 1))
        fi

        # ipv6
        # slaac:  ipv6_slaac
        # └─enable_other_flag: ipv6_dhcpv6-stateless
        # dhcpv6: ipv6_dhcpv6-stateful

        # ipv6
        if is_slaac; then
            if $recognize_ipv6_types; then
                if is_enable_other_flag; then
                    type=ipv6_dhcpv6-stateless
                else
                    type=ipv6_slaac
                fi
            else
                type=dhcp6
            fi
            yq -i ".network.config[$config_id].subnets[$subnet_id] = {\"type\": \"$type\"}" $ci_file

        elif is_dhcpv6; then
            if $recognize_ipv6_types; then
                type=ipv6_dhcpv6-stateful
            else
                type=dhcp6
            fi
            yq -i ".network.config[$config_id].subnets[$subnet_id] = {\"type\": \"$type\"}" $ci_file

        elif is_staticv6; then
            get_netconf_to ipv6_addr
            get_netconf_to ipv6_gateway
            if $recognize_static6; then
                type_ipv6_static=static6
            else
                type_ipv6_static=static
            fi
            yq -i ".network.config[$config_id].subnets[$subnet_id] = {
                    \"type\": \"$type_ipv6_static\",
                    \"address\": \"$ipv6_addr\",
                    \"gateway\": \"$ipv6_gateway\" }
                    " $ci_file
        fi
        # 无法设置 autoconf = false ?
        if should_disable_accept_ra; then
            yq -i ".network.config[$config_id].accept-ra = false" $ci_file
        fi

        # 有 ipv6 但需设置 dns 的情况
        if is_need_manual_set_dnsv6; then
            need_set_dns6=true
            for cur in $(get_current_dns 6); do
                yq -i ".network.config[$config_id].subnets[$subnet_id].dns_nameservers += [\"$cur\"]" $ci_file
            done
        fi

        config_id=$((config_id + 1))
    done

    if $need_set_dns4 || $need_set_dns6; then
        yq -i ".network.config[$config_id].type=\"nameserver\"" $ci_file
        if $need_set_dns4; then
            for cur in $(get_current_dns 4); do
                yq -i ".network.config[$config_id].address += [\"$cur\"]" $ci_file
            done
        fi
        if $need_set_dns6; then
            for cur in $(get_current_dns 6); do
                yq -i ".network.config[$config_id].address += [\"$cur\"]" $ci_file
            done
        fi
        # 如果 network.config[$config_id] 没有 address,则删除,避免低版本 cloud-init 报错
        yq -i "del(.network.config[$config_id] | select(has(\"address\") | not))" $ci_file
    fi

    apk del yq-go

    # 查看文件
    info "Cloud-init network config"
    cat -n $ci_file >&2
}

# 实测没用,生成的 machine-id 是固定的
# 而且 lightsail centos 9 模板 machine-id 也是相同的,显然相同 id 不是个问题
clear_machine_id() {
    os_dir=$1

    # https://www.freedesktop.org/software/systemd/man/latest/machine-id.html
    # gentoo 不会自动创建该文件
    echo uninitialized >$os_dir/etc/machine-id

    # https://build.opensuse.org/projects/Virtualization:Appliances:Images:openSUSE-Leap-15.5/packages/kiwi-templates-Minimal/files/config.sh?expand=1
    rm -f $os_dir/var/lib/systemd/random-seed
}

# 注意 anolis 7 有这个文件,可能干扰我们的配置?
# /etc/cloud/cloud.cfg.d/aliyun_cloud.cfg -> /sys/firmware/qemu_fw_cfg/by_name/etc/cloud-init/vendor-data/raw
download_cloud_init_config() {
    os_dir=$1
    recognize_static6=$2
    recognize_ipv6_types=$3

    ci_file=$os_dir/etc/cloud/cloud.cfg.d/99_fallback.cfg
    download $confhome/cloud-init.yaml $ci_file
    # 删除注释行,除了第一行
    sed -i '1!{/^[[:space:]]*#/d}' $ci_file

    # 修改密码
    # 不能用 sed 替换,因为含有特殊字符
    content=$(cat $ci_file)
    echo "${content//@PASSWORD@/$(get_password_linux_sha512)}" >$ci_file

    # 修改 ssh 端口
    if is_need_change_ssh_port; then
        sed -i "s/@SSH_PORT@/$ssh_port/g" $ci_file
    else
        sed -i "/@SSH_PORT@/d" $ci_file
    fi

    # swapfile
    # 如果分区表中已经有swapfile就跳过,例如arch
    if ! grep -w swap $os_dir/etc/fstab; then
        cat <>$ci_file
swap:
  filename: /swapfile
  size: auto
EOF
    fi

    create_cloud_init_network_config "$ci_file" "$recognize_static6" "$recognize_ipv6_types"
}

get_image_state() {
    local os_dir=$1
    local image_state=

    # 如果 dd 镜像精简了 State.ini,则从注册表获取
    if state_ini=$(find_file_ignore_case $os_dir/Windows/Setup/State/State.ini); then
        image_state=$(grep -i '^ImageState=' $state_ini | cut -d= -f2 | tr -d '\r')
    fi
    if [ -z "$image_state" ]; then
        apk add hivex
        hive=$(find_file_ignore_case $os_dir/Windows/System32/config/SOFTWARE)
        image_state=$(hivexget $hive '\Microsoft\Windows\CurrentVersion\Setup\State' ImageState)
        apk del hivex
    fi

    if [ -n "$image_state" ]; then
        echo "$image_state"
    else
        error_and_exit "Cannot get ImageState."
    fi
}

modify_windows() {
    os_dir=$1
    info "Modify Windows"

    # https://learn.microsoft.com/windows-hardware/manufacture/desktop/windows-setup-states
    # https://learn.microsoft.com/troubleshoot/azure/virtual-machines/reset-local-password-without-agent
    # https://learn.microsoft.com/windows-hardware/manufacture/desktop/add-a-custom-script-to-windows-setup

    # 判断用 SetupComplete 还是组策略
    image_state=$(get_image_state "$os_dir")
    echo "ImageState: $image_state"

    if [ "$image_state" = IMAGE_STATE_COMPLETE ]; then
        use_gpo=true
    else
        use_gpo=false
    fi

    # bat 列表
    bats=

    # 1. rdp 端口
    if is_need_change_rdp_port; then
        create_win_change_rdp_port_script $os_dir/windows-change-rdp-port.bat "$rdp_port"
        bats="$bats windows-change-rdp-port.bat"
    fi

    # 2. 允许 ping
    if is_allow_ping; then
        download $confhome/windows-allow-ping.bat $os_dir/windows-allow-ping.bat
        bats="$bats windows-allow-ping.bat"
    fi

    # 3. 合并分区
    # 可能 unattend.xml 已经设置了ExtendOSPartition,不过运行resize没副作用
    download $confhome/windows-resize.bat $os_dir/windows-resize.bat
    bats="$bats windows-resize.bat"

    # 4. 网络设置
    for ethx in $(get_eths); do
        create_win_set_netconf_script $os_dir/windows-set-netconf-$ethx.bat
        bats="$bats windows-set-netconf-$ethx.bat"
    done

    # 5 frp
    if ls /configs/frpc.* >/dev/null 2>&1; then
        if [ "$(get_windows_arch_from_windows_drive "$os_dir" | to_lower)" = x86 ]; then
            os_bit=32
        else
            os_bit=64
        fi
        mkdir -p "$os_dir/frpc/"
        url=$(get_frpc_url windows "$nt_ver" "$os_bit")
        download "$url" $os_dir/frpc/frpc.zip
        # -j 去除文件夹
        # -C 筛选文件时不区分大小写,但 busybox zip 不支持
        unzip -o -j "$os_dir/frpc/frpc.zip" '*/frpc.exe' -d "$os_dir/frpc/"
        rm -f "$os_dir/frpc/frpc.zip"
        cp -f /configs/frpc.* "$os_dir/frpc/"
        download "$confhome/windows-frpc.xml" "$os_dir/frpc/frpc.xml"
        download "$confhome/windows-frpc.bat" "$os_dir/frpc/frpc.bat"
        bats="$bats frpc\frpc.bat"
    fi

    if $use_gpo; then
        # 使用组策略
        scripts_ini=$(get_path_in_correct_case $os_dir/Windows/System32/GroupPolicy/Machine/Scripts/scripts.ini)
        mkdir -p "$(dirname $scripts_ini)"
        gpt_ini=$(get_path_in_correct_case $os_dir/Windows/System32/GroupPolicy/gpt.ini)

        # 备份 ini
        for file in $gpt_ini $scripts_ini; do
            if [ -f $file ]; then
                cp $file $file.orig
            fi
        done

        # gpt.ini
        cat >$gpt_ini <>$scripts_ini
        fi

        # 注意没用 pipefail 的话,错误码取自最后一个管道
        if num=$(grep -Eo '^[0-9]+' $scripts_ini | sort -n | tail -1 | grep .); then
            num=$((num + 1))
        else
            num=0
        fi

        bats="$bats windows-del-gpo.bat"
        for bat in $bats; do
            echo "${num}CmdLine=%SystemDrive%\\$bat" >>$scripts_ini
            echo "${num}Parameters=" >>$scripts_ini
            num=$((num + 1))
        done
        cat $scripts_ini
        unix2dos $scripts_ini

        # windows-del-gpo.bat
        download $confhome/windows-del-gpo.bat $os_dir/windows-del-gpo.bat
    else
        # 使用 SetupComplete
        setup_complete=$(get_path_in_correct_case $os_dir/Windows/Setup/Scripts/SetupComplete.cmd)
        mkdir -p "$(dirname $setup_complete)"

        # 添加到 C:\Setup\Scripts\SetupComplete.cmd 最前面
        # call 防止子 bat 删除自身后中断主脚本
        setup_complete_mod=$(mktemp)
        for bat in $bats; do
            echo "if exist %SystemDrive%\\$bat (call %SystemDrive%\\$bat)" >>$setup_complete_mod
        done

        # 复制原来的内容
        if [ -f $setup_complete ]; then
            cat $setup_complete >>$setup_complete_mod
        fi

        unix2dos $setup_complete_mod

        # cat 可以保留权限
        cat $setup_complete_mod >$setup_complete

        # 查看最终内容
        cat -n $setup_complete
    fi
}

get_axx64() {
    case "$(uname -m)" in
    x86_64) echo amd64 ;;
    aarch64) echo arm64 ;;
    esac
}

is_file_or_link() {
    # -e / -f 坏软连接,返回 false
    # -L 坏软连接,返回 true
    [ -f $1 ] || [ -L $1 ]
}

cp_resolv_conf() {
    os_dir=$1
    if is_file_or_link $os_dir/etc/resolv.conf &&
        ! is_file_or_link $os_dir/etc/resolv.conf.orig; then
        mv $os_dir/etc/resolv.conf $os_dir/etc/resolv.conf.orig
    fi
    cp -f /etc/resolv.conf $os_dir/etc/resolv.conf
}

rm_resolv_conf() {
    os_dir=$1
    rm -f $os_dir/etc/resolv.conf $os_dir/etc/resolv.conf.orig
}

restore_resolv_conf() {
    os_dir=$1
    if is_file_or_link $os_dir/etc/resolv.conf.orig; then
        mv -f $os_dir/etc/resolv.conf.orig $os_dir/etc/resolv.conf
    fi
}

keep_now_resolv_conf() {
    os_dir=$1
    rm -f $os_dir/etc/resolv.conf.orig
}

# 抄 https://github.com/alpinelinux/alpine-conf/blob/3.18.1/setup-disk.in#L421
get_alpine_firmware_pkgs() {
    # 需要有 modloop,不然 modinfo 会报错
    ensure_service_started modloop >&2

    # 如果不在单独的文件夹,则用 linux-firmware-other
    # 如果在单独的文件夹,则用 linux-firmware-xxx
    # 如果不需要 firmware,则用 linux-firmware-none
    firmware_pkgs=$(
        cd /sys/module && modinfo -F firmware -- * 2>/dev/null |
            awk -F/ '{print $1 == $0 ? "linux-firmware-other" : "linux-firmware-"$1}' |
            sort -u
    )

    # 使用 command 因为自己覆盖了 apk 添加了 >&2
    retry 5 command apk search --quiet --exact ${firmware_pkgs:-linux-firmware-none}
}

get_ucode_firmware_pkgs() {
    is_virt && return

    case "$distro" in
    centos | almalinux | rocky | oracle | redhat | anolis | opencloudos | openeuler) os=elol ;;
    *) os=$distro ;;
    esac

    case "$os-$(get_cpu_vendor)" in
    # alpine 的 linux-firmware 以文件夹进行拆分
    # setup-alpine 会自动安装需要的 firmware(modloop 没挂载则无效)
    # https://github.com/alpinelinux/alpine-conf/blob/3.18.1/setup-disk.in#L421
    alpine-intel) echo intel-ucode ;;
    alpine-amd) echo amd-ucode ;;
    alpine-*) ;;

    debian-intel) echo firmware-linux intel-microcode ;;
    debian-amd) echo firmware-linux amd64-microcode ;;
    debian-*) echo firmware-linux ;;

    ubuntu-intel) echo linux-firmware intel-microcode ;;
    ubuntu-amd) echo linux-firmware amd64-microcode ;;
    ubuntu-*) echo linux-firmware ;;

    # 无法同时安装 kernel-firmware kernel-firmware-intel
    opensuse-intel) echo kernel-firmware ucode-intel ;;
    opensuse-amd) echo kernel-firmware ucode-amd ;;
    opensuse-*) echo kernel-firmware ;;

    arch-intel) echo linux-firmware intel-ucode ;;
    arch-amd) echo linux-firmware amd-ucode ;;
    arch-*) echo linux-firmware ;;

    gentoo-intel) echo linux-firmware intel-microcode ;;
    gentoo-amd) echo linux-firmware ;;
    gentoo-*) echo linux-firmware ;;

    nixos-intel) echo linux-firmware microcodeIntel ;;
    nixos-amd) echo linux-firmware microcodeAmd ;;
    nixos-*) echo linux-firmware ;;

    fedora-intel) echo linux-firmware microcode_ctl ;;
    fedora-amd) echo linux-firmware amd-ucode-firmware microcode_ctl ;;
    fedora-*) echo linux-firmware microcode_ctl ;;

    elol-intel) echo linux-firmware microcode_ctl ;;
    elol-amd) echo linux-firmware microcode_ctl ;;
    elol-*) echo linux-firmware microcode_ctl ;;
    esac
}

chroot_systemctl_disable() {
    os_dir=$1
    shift

    for unit in "$@"; do
        # 如果传进来的是x(没有.) 则改成 x.service
        if ! [[ "$unit" = "*.*" ]]; then
            unit=$i.service
        fi

        # debian 10 返回值始终是 0
        if ! chroot $os_dir systemctl list-unit-files "$unit" 2>&1 | grep -Eq '^0 unit'; then
            chroot $os_dir systemctl disable "$unit"
        fi
    done
}

remove_or_disable_cloud_init() {
    os_dir=$1

    if ! is_have_cmd_on_disk $os_dir cloud-init; then
        return
    fi

    info "Remove or Disable Cloud-Init"

    # ubuntu-server-minimal ubuntu-cloud-minimal 都包含 cloud-init
    # 用 iso 安装的 ubuntu 也有 cloud-init
    # 因此不删除 ubuntu 的 cloud-init,而是禁用它

    # iso 安装首次启动是通过 /etc/cloud/cloud.cfg.d/99-installer.cfg 初始化系统,包括:
    #     1. 创建普通用户和密码,添加 ssh 登录公钥
    #     2. 创建 /etc/cloud/cloud-init.disabled

    if grep -iq ubuntu $os_dir/etc/os-release; then
        # 模仿 iso 安装的 ubuntu,只创建 cloud-init.disabled,不禁用服务
        touch $os_dir/etc/cloud/cloud-init.disabled
    else
        # systemctl is-enabled cloud-init-hotplugd.service 状态是 static
        # disable 会出现一堆提示信息,也无法 disable
        for unit in $(
            chroot $os_dir systemctl list-unit-files |
                grep -E '^(cloud-init|cloud-init-.*|cloud-config|cloud-final)\.(service|socket)' | grep enabled | awk '{print $1}'
        ); do
            # 服务不存在时会报错
            if chroot $os_dir systemctl -q is-enabled "$unit"; then
                chroot $os_dir systemctl disable "$unit"
            fi
        done

        for pkg_mgr in dnf yum zypper apt-get; do
            if is_have_cmd_on_disk $os_dir $pkg_mgr; then
                case $pkg_mgr in
                dnf | yum)
                    chroot $os_dir $pkg_mgr remove -y cloud-init
                    rm -f $os_dir/etc/cloud/cloud.cfg.rpmsave
                    ;;
                zypper)
                    # 加上 -u 才会删除依赖
                    chroot $os_dir zypper remove -y -u cloud-init cloud-init-config-suse
                    ;;
                apt-get)
                    # ubuntu 25.04 开始有 cloud-init-base
                    chroot_apt_remove $os_dir cloud-init cloud-init-base
                    chroot_apt_autoremove $os_dir
                    ;;
                esac
                break
            fi
        done
    fi
}

disable_jeos_firstboot() {
    os_dir=$1
    info "Disable JeOS Firstboot"

    # 两种方法都可以
    # https://github.com/openSUSE/jeos-firstboot?tab=readme-ov-file#usage

    rm -rf $os_dir/var/lib/YaST2/reconfig_system

    for name in jeos-firstboot jeos-firstboot-snapshot; do
        # 服务不存在时会报错
        chroot $os_dir systemctl disable "$name.service" 2>/dev/null || true
    done

    # 可选
    # chroot $os_dir zypper remove -y -u jeos-firstboot
}

create_network_manager_config() {
    source_cfg=$1
    os_dir=$2
    info "Create Network-Manager config"

    # 可以直接用 alpine 的 cloud-init 生成 Network Manager 配置
    apk add cloud-init
    cloud-init devel net-convert -p "$source_cfg" -k yaml -d /out -D alpine -O network-manager

    # 文档明确写了 ipv6.method=dhcp 无法获取网关
    # https://networkmanager.dev/docs/api/latest/nm-settings-nmcli.html#:~:text=false/no/off-,ipv6,-.method
    sed -i -e '/^may-fail=/d' -e 's/^method=dhcp/method=auto/' \
        /out/etc/NetworkManager/system-connections/cloud-init-eth*.nmconnection

    # 删除 # Generated by cloud-init. Changes will be lost.
    # 删除 org.freedesktop.NetworkManager.origin=cloud-init
    # 并删除头部的空行
    sed -i \
        -e '/^# Generated by cloud-init/d' \
        -e '/^org\.freedesktop\.NetworkManager\.origin=cloud-init/d' \
        /out/etc/NetworkManager/system-connections/cloud-init-eth*.nmconnection
    del_head_empty_lines_inplace /out/etc/NetworkManager/system-connections/cloud-init-eth*.nmconnection

    cp /out/etc/NetworkManager/system-connections/cloud-init-eth*.nmconnection \
        $os_dir/etc/NetworkManager/system-connections/

    # 清理
    rm -rf /out
    apk del cloud-init

    # 最终显示文件
    for file in "$os_dir"/etc/NetworkManager/system-connections/cloud-init-eth*.nmconnection; do
        cat -n "$file" >&2
    done
}

modify_linux() {
    os_dir=$1
    info "Modify Linux"

    find_and_mount() {
        mount_point=$1
        mount_dev=$(awk "\$2==\"$mount_point\" {print \$1}" $os_dir/etc/fstab)
        if [ -n "$mount_dev" ]; then
            mount $mount_dev $os_dir$mount_point
        fi
    }

    # 修复 onlink 网关
    add_onlink_script_if_need() {
        for ethx in $(get_eths); do
            if is_staticv4 || is_staticv6; then
                fix_sh=cloud-init-fix-onlink.sh
                download "$confhome/$fix_sh" "$os_dir/$fix_sh"
                insert_into_file "$ci_file" after '^runcmd:' <>> Running %preun scriptlet: netcat-0:1.229-3.fc43.x86_64
        # >>> Error in %preun scriptlet: netcat-0:1.229-3.fc43.x86_64
        # >>> Scriptlet output:
        # >>> failed to create admindir: No such file or directory
        # >>> [RPM] %preun(netcat-1.229-3.fc43.x86_64) scriptlet failed, exit status 2
        # >>> [RPM] netcat-1.229-3.fc43.x86_64: erase failed
        if [ "$distro" = fedora ] && [ "$releasever" = 43 ]; then
            chroot $os_dir dnf mark user netcat -y
        fi
        remove_or_disable_cloud_init $os_dir

        disable_selinux $os_dir
        disable_kdump $os_dir

        if fw_pkgs=$(get_ucode_firmware_pkgs) && [ -n "$fw_pkgs" ]; then
            is_have_cmd_on_disk $os_dir dnf && mgr=dnf || mgr=yum
            chroot $os_dir $mgr install -y $fw_pkgs
        fi

        restore_resolv_conf $os_dir
    fi

    # debian
    # 1. EOL 换源
    # 2. 修复网络问题
    # 3. 添加微码+固件
    # 注意 ubuntu 也有 /etc/debian_version
    if [ "$distro" = debian ]; then
        # 修复 onlink 网关
        # add_onlink_script_if_need

        mount_pseudo_fs $os_dir
        cp_resolv_conf $os_dir
        find_and_mount /boot
        find_and_mount /boot/efi

        remove_or_disable_cloud_init $os_dir

        # 获取当前开启的 Components, 后面要用
        if [ -f $os_dir/etc/apt/sources.list.d/debian.sources ]; then
            comps=$(grep ^Components: $os_dir/etc/apt/sources.list.d/debian.sources | head -1 | cut -d' ' -f2-)
        else
            comps=$(grep '^deb ' $os_dir/etc/apt/sources.list | head -1 | cut -d' ' -f4-)
        fi

        # ELTS/CN 源处理
        if is_elts; then
            # ELTS
            wget https://deb.freexian.com/extended-lts/archive-key.gpg \
                -O $os_dir/etc/apt/trusted.gpg.d/freexian-archive-extended-lts.gpg

            # shellcheck disable=SC1091
            codename=$({ . "$os_dir/etc/os-release" && echo "$VERSION_CODENAME"; })
            if [ -f $os_dir/etc/apt/sources.list.d/debian.sources ]; then
                cat <$os_dir/etc/apt/sources.list.d/debian.sources
Types: deb
URIs: http://$deb_mirror
Suites: $codename
Components: $comps
Signed-By: /etc/apt/trusted.gpg.d/freexian-archive-extended-lts.gpg
EOF
            else
                echo "deb http://$deb_mirror $codename $comps" >$os_dir/etc/apt/sources.list
            fi
        else
            # non-ELTS
            if is_in_china; then
                # 不处理 security 源 security.debian.org/debian-security 和 /etc/apt/mirrors/debian-security.list
                for file in $os_dir/etc/apt/mirrors/debian.list $os_dir/etc/apt/sources.list; do
                    if [ -f "$file" ]; then
                        sed -i "s|deb\.debian\.org/debian|$deb_mirror|" "$file"
                    fi
                done
            fi
        fi

        # 标记所有内核为自动安装
        pkgs=$(chroot $os_dir apt-mark showmanual linux-image* linux-headers*)
        chroot $os_dir apt-mark auto $pkgs

        # 安装合适的内核
        kernel_package=$kernel
        # shellcheck disable=SC2046
        # 检测机器是否能用 cloud 内核
        if [[ "$kernel_package" = 'linux-image-cloud-*' ]] &&
            ! sh /can_use_cloud_kernel.sh "$xda" $(get_eths); then
            kernel_package=$(echo "$kernel_package" | sed 's/-cloud//')
        fi

        # 该方法包含了 apt-mark manual
        chroot_apt_install $os_dir "$kernel_package"

        # 使用 autoremove 删除非最佳内核
        chroot_apt_autoremove $os_dir

        # 微码+固件
        if fw_pkgs=$(get_ucode_firmware_pkgs) && [ -n "$fw_pkgs" ]; then
            #  debian 10 11 的 iucode-tool 在 contrib 里面
            #  debian 12 的 iucode-tool 在 main 里面
            [ "$releasever" -ge 12 ] &&
                comps_to_add=non-free-firmware ||
                comps_to_add="contrib non-free"

            if [ -f $os_dir/etc/apt/sources.list.d/debian.sources ]; then
                file=$os_dir/etc/apt/sources.list.d/debian.sources
                search='^[# ]*Components:'
            else
                file=$os_dir/etc/apt/sources.list
                search='^[# ]*deb'
            fi

            for c in $comps_to_add; do
                if ! echo "$comps" | grep -wq "$c"; then
                    sed -Ei "/$search/s/$/ $c/" $file
                fi
            done

            chroot_apt_install $os_dir $fw_pkgs
        fi

        # genericcloud 删除以下文件开机时才会显示 grub 菜单
        # https://salsa.debian.org/cloud-team/debian-cloud-images/-/tree/master/config_space/bookworm/files/etc/default/grub.d
        rm -f $os_dir/etc/default/grub.d/10_cloud.cfg
        rm -f $os_dir/etc/default/grub.d/15_timeout.cfg
        chroot $os_dir update-grub

        if true; then
            # 如果使用 nocloud 镜像
            chroot_apt_install $os_dir openssh-server
        else
            # 如果使用 genericcloud 镜像

            # 还原默认配置并创建 key
            # cat $os_dir/usr/share/openssh/sshd_config $os_dir/etc/ssh/sshd_config
            # chroot $os_dir ssh-keygen -A
            rm -rf $os_dir/etc/ssh/sshd_config
            UCF_FORCE_CONFFMISS=1 chroot $os_dir dpkg-reconfigure openssh-server
        fi

        # 镜像自带的网络管理器
        # debian 11 ifupdown
        # debian 12 netplan + networkd + resolved
        # ifupdown dhcp 不支持 24位掩码+不规则网关?

        # 强制使用 netplan
        if false && is_have_cmd_on_disk $os_dir netplan; then
            chroot_apt_install $os_dir netplan.io
            # 服务不存在时会报错
            chroot $os_dir systemctl disable networking resolvconf 2>/dev/null || true
            chroot $os_dir systemctl enable systemd-networkd systemd-resolved
            rm_resolv_conf $os_dir
            ln -sf ../run/systemd/resolve/stub-resolv.conf $os_dir/etc/resolv.conf
            if [ -f "$os_dir/etc/cloud/cloud.cfg.d/99_fallback.cfg" ]; then
                insert_into_file $os_dir/etc/cloud/cloud.cfg.d/99_fallback.cfg after '#cloud-config' </dev/null || true

        chroot_apt_install $os_dir ifupdown
        chroot_apt_remove $os_dir resolvconf netplan.io systemd-resolved
        chroot_apt_autoremove $os_dir
        chroot $os_dir systemctl enable networking

        # 静态时 networking 服务不会根据 /etc/network/interfaces 更新 resolv.conf
        # 动态时使用了 isc-dhcp-client 支持自动更新 resolv.conf
        # 另外 debian iso 不会安装 rdnssd
        keep_now_resolv_conf $os_dir
    fi

    # opensuse
    # 1. kernel-default-base 缺少 nvme gve mlx5 mana 驱动,换成 kernel-default
    # 2. 添加微码+固件
    # https://documentation.suse.com/smart/virtualization-cloud/html/minimal-vm/index.html
    if grep -q opensuse $os_dir/etc/os-release; then
        create_swap_if_ram_less_than 1024 $os_dir/swapfile
        mount_pseudo_fs $os_dir
        cp_resolv_conf $os_dir
        find_and_mount /boot
        find_and_mount /boot/efi

        disable_jeos_firstboot $os_dir

        # 禁用 selinux
        disable_selinux $os_dir

        # opensuse leap 15.6 用 wicked
        # opensuse leap 16.0 / tumbleweed 用 NetworkManager
        if chroot $os_dir rpm -qi wicked; then
            # sysconfig ifcfg
            create_cloud_init_network_config $os_dir/net.cfg
            chroot $os_dir cloud-init devel net-convert \
                -p /net.cfg -k yaml -d out -D opensuse -O sysconfig

            # 删除
            # Created by cloud-init on instance boot automatically, do not edit.
            #
            sed -i '/^#/d' "$os_dir/out/etc/sysconfig/network/ifcfg-eth"*

            for ethx in $(get_eths); do
                # 1. 修复甲骨文云重启后 ipv6 丢失
                # https://github.com/openSUSE/wicked/issues/1058
                # 还要注意 wicked dhcpv6 获取到的 ipv6 是 /64,其他 DHCPv6 程序获取到的是 /128
                echo DHCLIENT6_USE_LAST_LEASE=no >>$os_dir/out/etc/sysconfig/network/ifcfg-$ethx

                # 2. 修复 onlink 网关
                for prefix in '' 'default '; do
                    if is_staticv4; then
                        get_netconf_to ipv4_gateway
                        echo "${prefix}${ipv4_gateway} - -" >>$os_dir/out/etc/sysconfig/network/ifroute-$ethx
                    fi
                    if is_staticv6; then
                        get_netconf_to ipv6_gateway
                        echo "${prefix}${ipv6_gateway} - -" >>$os_dir/out/etc/sysconfig/network/ifroute-$ethx
                    fi
                done
            done

            # 复制配置
            for file in \
                "$os_dir/out/etc/sysconfig/network/ifcfg-eth"* \
                "$os_dir/out/etc/sysconfig/network/ifroute-eth"*; do
                # 动态 ip 没有 ifroute-eth*
                if [ -f $file ]; then
                    cp $file $os_dir/etc/sysconfig/network/
                fi
            done

            # 清理
            rm -rf $os_dir/net.cfg $os_dir/out

        else
            # 如果使用 cloud-init 则需要 touch NetworkManager.conf
            # 更新到 cloud-init 24.1 后删除
            # touch $os_dir/etc/NetworkManager/NetworkManager.conf

            # 可以直接用 alpine 的 cloud-init 生成 Network Manager 配置
            create_cloud_init_network_config /net.cfg
            create_network_manager_config /net.cfg "$os_dir"
            rm /net.cfg
        fi

        # 选择新内核
        # 只有 leap 有 kernel-azure
        if grep -iq leap $os_dir/etc/os-release && [ "$(get_cloud_vendor)" = azure ]; then
            target_kernel='kernel-azure'
        else
            target_kernel='kernel-default'
        fi

        # rpm -qi 不支持通配符
        origin_kernel=$(chroot $os_dir rpm -qa 'kernel-*' --qf '%{NAME}\n' | grep -v firmware)
        if ! [ "$(echo "$origin_kernel" | wc -l)" -eq 1 ]; then
            error_and_exit "Unexpected kernel installed: $origin_kernel"
        fi

        # 16.0 能同时装 kernel-default-base 和 kernel-default
        # tw 不能同时装 kernel-default-base 和 kernel-default
        # 因此需要添加 --force-resolution 自动删除 kernel-default-base
        if ! [ "$origin_kernel" = "$target_kernel" ]; then
            # x86 必须设置一个密码,否则报错,arm 没有这个问题
            # Failed to get root password hash
            # Failed to import /etc/uefi/certs/76B6A6A0.crt
            # warning: %post(kernel-default-5.14.21-150500.55.83.1.x86_64) scriptlet failed, exit status 255
            need_password_workaround=false
            if grep -q '^root:[:!*]' $os_dir/etc/shadow; then
                need_password_workaround=true
            fi

            if $need_password_workaround; then
                echo "root:$(mkpasswd '')" | chroot $os_dir chpasswd -e
            fi
            # 安装新内核
            chroot $os_dir zypper install -y --force-resolution $target_kernel
            # 删除旧内核
            if chroot $os_dir rpm -q $origin_kernel; then
                chroot $os_dir zypper remove -y --force-resolution $origin_kernel
            fi
            if $need_password_workaround; then
                chroot $os_dir passwd -d root
            fi
        fi

        # 固件+微码
        if fw_pkgs=$(get_ucode_firmware_pkgs) && [ -n "$fw_pkgs" ]; then
            chroot $os_dir zypper install -y $fw_pkgs
        fi

        # 最后才删除 cloud-init
        # 因为生成 sysconfig 网络配置要用目标系统的 cloud-init
        remove_or_disable_cloud_init $os_dir

        restore_resolv_conf $os_dir
    fi

    # arch 云镜像
    if false && [ -f $os_dir/etc/arch-release ]; then
        # 修复 onlink 网关
        add_onlink_script_if_need

        # 同步证书
        cp_resolv_conf $os_dir
        mount_pseudo_fs $os_dir
        chroot $os_dir pacman-key --init
        chroot $os_dir pacman-key --populate
        rm_resolv_conf $os_dir
    fi

    # gentoo 云镜像
    if false && [ -f $os_dir/etc/gentoo-release ]; then
        # 挂载伪文件系统
        mount_pseudo_fs $os_dir
        cp_resolv_conf $os_dir

        # 在这里修改密码,而不是用cloud-init,因为我们的默认密码太弱
        is_password_plaintext && sed -i 's/enforce=everyone/enforce=none/' $os_dir/etc/security/passwdqc.conf
        change_root_password $os_dir
        is_password_plaintext && sed -i 's/enforce=none/enforce=everyone/' $os_dir/etc/security/passwdqc.conf

        # 下载仓库,选择 profile
        chroot $os_dir emerge-webrsync
        profile=$(chroot $os_dir eselect profile list | grep stable | grep systemd |
            awk '{print length($2), $2}' | sort -n | head -1 | awk '{print $2}')
        chroot $os_dir eselect profile set $profile

        # 删除 resolv.conf,不然 systemd-resolved 无法创建软链接
        rm_resolv_conf $os_dir

        # 启用网络服务
        chroot $os_dir systemctl enable systemd-networkd
        chroot $os_dir systemctl enable systemd-resolved

        # systemd-networkd 有时不会运行
        # https://bugs.gentoo.org/910404 补丁好像没用
        # https://github.com/systemd/systemd/issues/27718#issuecomment-1564877478
        # 临时的解决办法是运行 networkctl,如果启用了systemd-networkd服务,会运行服务
        insert_into_file $os_dir/lib/systemd/system/systemd-logind.service after '\[Service\]' <"$os_dir/etc/cloud/cloud.cfg.d/99-datasource.cfg" <<'EOF'
datasource_list: [ NoCloud, None ]
datasource:
  NoCloud:
    seedfrom: /var/lib/cloud/seed/nocloud/
    fs_label: null
EOF

    # 2. 复制 seed 文件(已在 host 上准备好,打包在 initrd 中)
    mkdir -p "$os_dir/var/lib/cloud/seed/nocloud"
    cp /configs/cloud-data/* "$os_dir/var/lib/cloud/seed/nocloud/"

    # 3. 确保 cloud-init 没有被禁用
    rm -f "$os_dir/etc/cloud/cloud-init.disabled"

    # 4. 清除 cloud-init 旧状态,确保首次启动重新执行
    rm -rf "$os_dir/var/lib/cloud/instance"
    rm -rf "$os_dir/var/lib/cloud/instances"
}

modify_os_on_disk() {
    only_process=$1
    info "Modify disk if is $only_process"

    update_part

    # dd linux 的时候不用修改硬盘内容(nocloud 模式除外)
    if [ "$distro" = "dd" ] && [ "$only_process" != "nocloud" ] && ! lsblk -f /dev/$xda | grep ntfs; then
        return
    fi

    mkdir -p /os
    # 按分区容量大到小,依次寻找系统分区
    for part in $(lsblk /dev/$xda*[0-9] --sort SIZE -no NAME | tac); do
        # btrfs挂载的是默认子卷,如果没有默认子卷,挂载的是根目录
        # fedora 云镜像没有默认子卷,且系统在root子卷中
        if mount -o ro /dev/$part /os; then
            if [ "$only_process" = linux ] || [ "$only_process" = nocloud ]; then
                if etc_dir=$({ ls -d /os/etc/ || ls -d /os/*/etc/; } 2>/dev/null); then
                    os_dir=$(dirname $etc_dir)
                    # 重新挂载为读写
                    mount -o remount,rw /os
                    if [ "$only_process" = nocloud ]; then
                        setup_nocloud $os_dir
                    else
                        modify_linux $os_dir
                    fi
                    return
                fi
            elif [ "$only_process" = windows ]; then
                # find 不是很聪明
                # find /mnt/c -iname windows -type d -maxdepth 1
                # find: /mnt/c/pagefile.sys: Permission denied
                # find: /mnt/c/swapfile.sys: Permission denied
                # shellcheck disable=SC1090
                # find_file_ignore_case 也在这个文件里面
                . <(wget -O- $confhome/windows-driver-utils.sh)
                if find_file_ignore_case /os/Windows/System32/ntoskrnl.exe >/dev/null 2>&1; then
                    # 其他地方会用到
                    is_windows() { true; }
                    # 重新挂载为读写、忽略大小写
                    umount /os
                    if ! { mount -t ntfs3 -o nocase,rw /dev/$part /os &&
                        mount | grep -w 'on /os type' | grep -wq rw; }; then
                        # 显示警告
                        warn "Can't normally mount windows partition /dev/$part as rw."
                        dmesg | grep -F "ntfs3($part):" || true
                        # 有可能 fallback 挂载成 ro, 因此先取消挂载
                        if mount | grep -wq 'on /os type'; then
                            umount /os
                        fi
                        # 尝试修复并强制挂载
                        apk add ntfs-3g-progs
                        ntfsfix /dev/$part
                        apk del ntfs-3g-progs
                        mount -t ntfs3 -o nocase,rw,force /dev/$part /os
                    fi
                    # 获取版本号,其他地方会用到
                    get_windows_version_from_windows_drive /os
                    modify_windows /os
                    return
                fi
            fi
            umount /os
        fi
    done
    error_and_exit "Can't find os partition."
}

get_need_swap_size() {
    need_ram=$1
    phy_ram=$(get_approximate_ram_size)

    if [ $need_ram -gt $phy_ram ]; then
        echo $((need_ram - phy_ram))
    else
        echo 0
    fi
}

create_swap_if_ram_less_than() {
    need_ram=$1
    swapfile=$2

    swapsize=$(get_need_swap_size $need_ram)
    if [ $swapsize -gt 0 ]; then
        create_swap $swapsize $swapfile
    fi
}

create_swap() {
    swapsize=$1
    swapfile=$2

    if ! grep $swapfile /proc/swaps; then
        # 用兼容 btrfs 的方式创建 swapfile
        truncate -s 0 $swapfile
        # 如果分区不支持 chattr +C 会显示错误但返回值是 0
        chattr +C $swapfile 2>/dev/null
        fallocate -l ${swapsize}M $swapfile
        chmod 0600 $swapfile
        mkswap $swapfile
        swapon $swapfile
    fi
}

set_ssh_keys_and_del_password() {
    os_dir=$1
    info 'set ssh keys'

    # 添加公钥
    (
        umask 077
        mkdir -p $os_dir/root/.ssh
        cat /configs/ssh_keys >$os_dir/root/.ssh/authorized_keys
    )

    # 删除密码
    chroot $os_dir passwd -d root
}

# 除了 alpine 都会用到
change_ssh_conf() {
    os_dir=$1
    key=$2
    value=$3
    sub_conf=$4

    if line="^$key .*" && grep -Exq "$line" $os_dir/etc/ssh/sshd_config 2>/dev/null; then
        # 如果 sshd_config 存在此 key(非注释状态),则替换
        sed -Ei "s/$line/$key $value/" $os_dir/etc/ssh/sshd_config
    elif include_line='^Include.*/etc/ssh/sshd_config.d' &&
        # arch 没有 /etc/ssh/sshd_config.d/ 文件夹
        # opensuse tumbleweed 没有 /etc/ssh/sshd_config
        #                       有 /etc/ssh/sshd_config.d/ 文件夹
        #                       有 /usr/etc/ssh/sshd_config
        { grep -q "$include_line" $os_dir/etc/ssh/sshd_config ||
            grep -q "$include_line" $os_dir/usr/etc/ssh/sshd_config; } 2>/dev/null; then
        mkdir -p $os_dir/etc/ssh/sshd_config.d/
        echo "$key $value" >"$os_dir/etc/ssh/sshd_config.d/$sub_conf"
    else
        # 如果 sshd_config 存在此 key (无论是否已注释),则替换,包括删除注释
        # 否则追加
        line="^[# ]*$key .*"
        if grep -Exq "$line" $os_dir/etc/ssh/sshd_config; then
            sed -Ei "s/$line/$key $value/" $os_dir/etc/ssh/sshd_config
        else
            echo "$key $value" >>$os_dir/etc/ssh/sshd_config
        fi
    fi
}

allow_password_login() {
    os_dir=$1
    change_ssh_conf "$os_dir" PasswordAuthentication yes 01-PasswordAuthentication.conf
}

allow_root_password_login() {
    os_dir=$1

    # opensuse 16/tumbleweed 安装 openssh-server-config-rootlogin
    # 会生成 /usr/etc/ssh/sshd_config.d/50-permit-root-login.conf
    # 但是如果用户删除了此文件,包有更新的话,可能会重新创建这个文件?
    # 因此先不用这个方法
    if false && [ -f $os_dir/etc/os-release ] &&
        grep -iq opensuse $os_dir/etc/os-release &&
        ! grep -iq 15.6 $os_dir/etc/os-release; then
        chroot $os_dir zypper install -y openssh-server-config-rootlogin
    else
        change_ssh_conf "$os_dir" PermitRootLogin yes 01-permitrootlogin.conf
    fi
}

change_ssh_port() {
    os_dir=$1
    ssh_port=$2

    change_ssh_conf "$os_dir" Port "$ssh_port" 01-change-ssh-port.conf
}

change_root_password() {
    os_dir=$1

    info 'change root password'

    if is_password_plaintext; then
        pam_d=$os_dir/etc/pam.d

        [ -f $pam_d/chpasswd ] && has_pamd_chpasswd=true || has_pamd_chpasswd=false

        if $has_pamd_chpasswd; then
            cp $pam_d/chpasswd $pam_d/chpasswd.orig

            # cat /etc/pam.d/chpasswd
            # @include common-password

            # cat /etc/pam.d/chpasswd
            # #%PAM-1.0
            # auth       include      system-auth
            # account    include      system-auth
            # password   substack     system-auth
            # -password   optional    pam_gnome_keyring.so use_authtok
            # password   substack     postlogin

            # 通过 /etc/pam.d/chpasswd 找到 /etc/pam.d/system-auth 或者 /etc/pam.d/system-auth
            # 再找到有 password 和 pam_unix.so 的行,并删除 use_authtok,写入 /etc/pam.d/chpasswd
            files=$(grep -E '^(password|@include)' $pam_d/chpasswd | awk '{print $NF}' | sort -u)
            for file in $files; do
                if [ -f "$pam_d/$file" ] && line=$(grep ^password "$pam_d/$file" | grep -F pam_unix.so); then
                    echo "$line" | sed 's/use_authtok//' >$pam_d/chpasswd
                    break
                fi
            done
        fi

        # 分两行写,不然遇到错误不会终止
        plaintext=$(get_password_plaintext)
        echo "root:$plaintext" | chroot $os_dir chpasswd

        if $has_pamd_chpasswd; then
            mv $pam_d/chpasswd.orig $pam_d/chpasswd
        fi
    else
        echo "root:$(get_password_linux_sha512)" | chroot $os_dir chpasswd -e
    fi
}

disable_selinux() {
    os_dir=$1

    # https://access.redhat.com/solutions/3176
    # centos7 也建议将 selinux 开关写在 cmdline
    # grep selinux=0 /usr/lib/dracut/modules.d/98selinux/selinux-loadpolicy.sh
    #     warn "To disable selinux, add selinux=0 to the kernel command line."
    if [ -f $os_dir/etc/selinux/config ]; then
        sed -i 's/^SELINUX=enforcing/SELINUX=disabled/g' $os_dir/etc/selinux/config
    fi

    # opensuse 没有安装 grubby
    if is_have_cmd_on_disk $os_dir grubby; then
        # grubby 只处理 GRUB_CMDLINE_LINUX,不会处理 GRUB_CMDLINE_LINUX_DEFAULT
        # rocky 的 GRUB_CMDLINE_LINUX_DEFAULT 有 crashkernel=auto
        chroot $os_dir grubby --update-kernel ALL --args selinux=0

        # el7 上面那条 grubby 命令不能设置 /etc/default/grub
        sed -i 's/selinux=1/selinux=0/' $os_dir/etc/default/grub
    else
        # 有可能没有 selinux 参数,但现在的镜像没有这个问题
        # sed -Ei 's/[[:space:]]?(security|selinux|enforcing)=[^ ]*//g' $os_dir/etc/default/grub
        sed -i 's/selinux=1/selinux=0/' $os_dir/etc/default/grub

        # 如果需要用 snapshot 可以用 transactional-update grub.cfg
        chroot $os_dir grub2-mkconfig -o /boot/grub2/grub.cfg
    fi
}

disable_kdump() {
    os_dir=$1

    # grubby 只处理 GRUB_CMDLINE_LINUX,不会处理 GRUB_CMDLINE_LINUX_DEFAULT
    # rocky 的 GRUB_CMDLINE_LINUX_DEFAULT 有 crashkernel=auto

    # 新安装的内核依然有 crashkernel,好像是 bug
    # https://forums.rockylinux.org/t/how-do-i-remove-crashkernel-from-cmdline/13346
    # 验证过程
    # yum remove --oldinstallonly   # 删除旧内核
    # rm -rf /boot/loader/entries/* # 删除启动条目
    # yum reinstall kernel-core     # 重新安装新内核
    # cat /boot/loader/entries/*    # 依然有 crashkernel=1G-4G:192M,4G-64G:256M,64G-:512M

    chroot $os_dir grubby --update-kernel ALL --args crashkernel=no
    # el7 上面那条 grubby 命令不能设置 /etc/default/grub
    sed -i 's/crashkernel=[^ "]*/crashkernel=no/' $os_dir/etc/default/grub
    if chroot $os_dir systemctl -q is-enabled kdump; then
        chroot $os_dir systemctl disable kdump
    fi
}

download_qcow() {
    apk add qemu-img
    info "Download qcow2 image"

    mkdir -p /installer
    mount /dev/disk/by-label/installer /installer

    qcow_file=/installer/cloud_image.qcow2
    if [ -n "$img_type_warp" ]; then
        # 边下载边解压,单线程下载
        # 用官方 wget ,带进度条
        apk add wget
        wget $img -O- | pipe_extract >$qcow_file
    else
        # 多线程下载
        download "$img" "$qcow_file"
    fi
}

connect_qcow() {
    modprobe nbd nbds_max=1
    qemu-nbd -c /dev/nbd0 $qcow_file

    # 需要等待一下
    # https://github.com/canonical/cloud-utils/blob/main/bin/mount-image-callback
    while ! blkid /dev/nbd0; do
        echo "Waiting for qcow file to be mounted..."
        sleep 5
    done
}

disconnect_qcow() {
    if [ -f /sys/block/nbd0/pid ]; then
        qemu-nbd -d /dev/nbd0

        # 需要等待一下
        while fuser -sm $qcow_file; do
            echo "Waiting for qcow file to be unmounted..."
            sleep 5
        done
    fi
}

get_part_size_mb_for_file_size_b() {
    local file_b=$1
    local file_mb=$((file_b / 1024 / 1024))

    # ext4 默认参数下
    #  分区大小   可用大小   利用率
    #  100 MiB      86 MiB   86.0%
    #  200 MiB     177 MiB   88.5%
    #  500 MiB     454 MiB   90.8%
    #  512 MiB     476 MiB   92.9%
    # 1024 MiB     957 MiB   93.4%
    # 2000 MiB    1914 MiB   95.7%
    # 2048 MiB    1929 MiB   94.1% 这里反而下降了
    # 5120 MiB    4938 MiB   96.4%

    # 文件系统大约占用 5% 空间

    # 假设 1929M 的文件,计算得到需要创建 2031M 的分区
    # 但是实测 2048M 的分区才能存放 1929M 的文件
    # 因此预留不足 150M 时补够 150M
    local reserve_mb=$((file_mb * 100 / 95 - file_mb))
    if [ $reserve_mb -lt 150 ]; then
        reserve_mb=150
    fi

    part_mb=$((file_mb + reserve_mb))
    echo "File size:      $file_mb MiB" >&2
    echo "Part size need: $part_mb MiB" >&2
    echo $part_mb
}

get_cloud_image_part_size() {
    # 7
    # https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud-2211.qcow2c 400m

    # 8
    # https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/AlmaLinux-8-GenericCloud-latest.x86_64.qcow2 600m
    # https://download.rockylinux.org/pub/rocky/8/images/x86_64/Rocky-8-GenericCloud-Base.latest.x86_64.qcow2 1.8g
    # https://yum.oracle.com/templates/OracleLinux/OL8/u9/x86_64/OL8U9_x86_64-kvm-b219.qcow2 1g
    # https://rhel-8.10-x86_64-kvm.qcow2 1g

    # 9
    # https://cloud.centos.org/centos/9-stream/x86_64/images/CentOS-Stream-GenericCloud-9-latest.x86_64.qcow2 1.2g
    # https://repo.almalinux.org/almalinux/9/cloud/x86_64/images/AlmaLinux-9-GenericCloud-latest.x86_64.qcow2 600m
    # https://download.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2 600m
    # https://yum.oracle.com/templates/OracleLinux/OL9/u3/x86_64/OL9U3_x86_64-kvm-b220.qcow2 600m
    # https://rhel-9.4-x86_64-kvm.qcow2 900m

    # 10
    # https://cloud.centos.org/centos/10-stream/x86_64/images/CentOS-Stream-GenericCloud-10-latest.x86_64.qcow2 900m

    # https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/cloud/nocloud_alpine-3.19.1-x86_64-uefi-cloudinit-r0.qcow2 200m
    # https://kali.download/cloud-images/current/kali-linux-2024.1-cloud-genericcloud-amd64.tar.xz 200m
    # https://download.opensuse.org/tumbleweed/appliances/openSUSE-Tumbleweed-Minimal-VM.x86_64-Cloud.qcow2 300m
    # https://download.opensuse.org/distribution/leap/15.5/appliances/openSUSE-Leap-15.5-Minimal-VM.aarch64-Cloud.qcow2 300m
    # https://mirror.fcix.net/fedora/linux/releases/40/Cloud/x86_64/images/Fedora-Cloud-Base-Generic.x86_64-40-1.14.qcow2 400m
    # https://geo.mirror.pkgbuild.com/images/latest/Arch-Linux-x86_64-cloudimg.qcow2 500m
    # https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2 500m
    # https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img 500m
    # https://gentoo.osuosl.org/experimental/amd64/openstack/gentoo-openstack-amd64-systemd-latest.qcow2 800m

    # openeuler 是 .qcow2.xz,要解压后才知道 qcow2 大小
    if [ "$distro" = openeuler ]; then
        # openeuler 20.03 3g
        if [ "$releasever" = 20.03 ]; then
            echo 3GiB
        else
            echo 2GiB
        fi
    elif size_bytes=$(get_http_file_size "$img"); then
        # 缩小 btrfs 需要写 qcow2 ,实测写入后只多了 1M,因此不用特殊处理
        echo "$(get_part_size_mb_for_file_size_b $size_bytes)MiB"
    else
        # 如果没获取到文件大小
        echo "Could not get cloud image size in http response." >&2
        echo 2GiB
    fi
}

chroot_dnf() {
    if is_have_cmd_on_disk /os/ dnf; then
        chroot /os/ dnf -y "$@"
    else
        chroot /os/ yum -y "$@"
    fi
}

chroot_apt_update() {
    local os_dir=$1

    current_hash=$(cat $os_dir/etc/apt/sources.list $os_dir/etc/apt/sources.list.d/*.sources 2>/dev/null | md5sum)
    if ! [ "$saved_hash" = "$current_hash" ]; then
        chroot $os_dir apt-get update
        saved_hash="$current_hash"
    fi
}

chroot_apt_install() {
    local os_dir=$1
    shift

    # 只安装未安装的软件包
    # 避免更新浪费时间
    local pkg='' pkgs=''
    for pkg in "$@"; do
        if chroot $os_dir dpkg -s "$pkg" >/dev/null 2>&1; then
            # 如果已安装则标记为 manual,防止被 autoremove 删除
            chroot $os_dir apt-mark manual "$pkg"
        else
            pkgs="$pkgs $pkg"
        fi
    done

    # 一次性安装,避免多次 update-initramfs
    if [ -n "$pkgs" ]; then
        chroot_apt_update $os_dir
        DEBIAN_FRONTEND=noninteractive chroot $os_dir apt-get install -y $pkgs
    fi
}

chroot_apt_remove() {
    local os_dir=$1
    shift

    # minimal 镜像 删除 grub-pc 时会安装 grub-efi-amd64
    # 因此需要先更新索引
    chroot_apt_update $os_dir

    # 不能用 apt remove --purge -y xxx yyy
    # 因为如果索引里没有其中一个,会报错,另一个也不会删除
    local pkgs=
    for pkg in "$@"; do
        # apt list 会提示 WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
        # 但又不能用 apt-get list
        if chroot $os_dir apt list --installed "$pkg" | grep -q installed; then
            pkgs="$pkgs $pkg"
        fi
    done

    # 删除 resolvconf 时会弹出建议重启,因此添加 noninteractive
    DEBIAN_FRONTEND=noninteractive chroot $os_dir apt-get remove --purge --allow-remove-essential -y $pkgs
}

chroot_apt_autoremove() {
    local os_dir=$1

    change_confs() {
        action=$1

        # 只有 16.04 有 01autoremove-kernels
        # 16.04 结束支持后删除
        for conf in 01autoremove 01autoremove-kernels; do
            file=$os_dir/etc/apt/apt.conf.d/$conf
            case "$action" in
            change)
                if [ -f $file ]; then
                    sed -i.orig 's/VersionedKernelPackages/x/; s/NeverAutoRemove/x/' $file
                fi
                ;;
            restore)
                if [ -f $file.orig ]; then
                    mv $file.orig $file
                fi
                ;;
            esac
        done
    }

    change_confs change
    DEBIAN_FRONTEND=noninteractive chroot $os_dir apt-get autoremove --purge -y
    change_confs restore
}

del_default_user() {
    os_dir=$1

    while read -r user; do
        if grep ^$user':\$' "$os_dir/etc/shadow"; then
            echo "Deleting user $user"
            chroot "$os_dir" userdel -rf "$user"
        fi
    done < <(grep -v nologin$ "$os_dir/etc/passwd" | cut -d: -f1 | grep -v root)
}

is_el7_family() {
    is_have_cmd_on_disk "$1" yum &&
        ! is_have_cmd_on_disk "$1" dnf
}

del_exist_sysconfig_NetworkManager_config() {
    os_dir=$1

    # 删除云镜像自带的 dhcp 配置,防止歧义
    rm -rf $os_dir/etc/NetworkManager/system-connections/*.nmconnection
    rm -rf $os_dir/etc/sysconfig/network-scripts/ifcfg-*

    # 1. 修复 cloud-init 添加了 IPV*_FAILURE_FATAL / may-fail=false
    #    甲骨文 dhcpv6 获取不到 IP 将视为 fatal,原有的 ipv4 地址也会被删除
    # 2. 修复 dhcpv6 下,ifcfg 添加了 IPV6_AUTOCONF=no 导致无法获取网关
    # 3. 修复 dhcpv6 下,NM method=dhcp 导致无法获取网关
    if false; then
        ci_file=$os_dir/etc/cloud/cloud.cfg.d/99_fallback.cfg

        insert_into_file $ci_file after '^runcmd:' < trim-install > trim-grub

    # 挂载 /os
    mkdir -p /os
    mount /dev/$xda*2 /os

    # 下载并挂载 iso
    mkdir -p /os/installer /iso
    download "$iso" /os/installer/fnos.iso
    mount -o ro /os/installer/fnos.iso /iso

    # 解压 initrd
    apk add cpio
    initrd_dir=/os/installer/initrd_dir
    mkdir -p $initrd_dir
    (
        cd $initrd_dir
        suffix=$(
            case $(uname -m) in
            x86_64) echo amd ;;
            aarch64) echo a64 ;;
            *) ;;
            esac
        )
        zcat /iso/install.$suffix/initrd.gz | cpio -idm
    )
    apk del cpio

    # 获取挂载参数
    fstab_line_os=$(strings $initrd_dir/trim-install | grep -m1 '^UUID=%s / ')
    fstab_line_efi=$(strings $initrd_dir/trim-install | grep -m1 '^UUID=%s /boot/efi ')
    fstab_line_swapfile=$(strings $initrd_dir/trim-install | grep -m1 '^/swapfile none swap ')

    # 删除 initrd
    rm -rf $initrd_dir

    # 复制 trimfs.tgz 并删除 ISO 以获得更多空间
    echo "moving trimfs.tgz..."
    cp /iso/trimfs.tgz /os/installer
    umount /iso
    rm /os/installer/fnos.iso

    # 挂载 /os/boot/efi
    if is_efi; then
        mkdir -p /os/boot/efi
        mount -o "$(echo "$fstab_line_efi" | awk '{print $4}')" /dev/$xda*1 /os/boot/efi
    fi

    # 复制系统
    info "Extract fnos"
    apk add tar gzip pv
    pv -f /os/installer/trimfs.tgz | tar zxp --numeric-owner --xattrs-include='*.*' -C /os
    apk del tar gzip pv

    # 删除 installer (trimfs.tgz)
    rm -rf /os/installer

    # 挂载 proc sys dev
    mount_pseudo_fs /os

    # 更改密码
    if is_need_set_ssh_keys; then
        set_ssh_keys_and_del_password $os_dir
    else
        change_root_password $os_dir
    fi

    # ssh root 登录,测试用
    if false; then
        allow_root_password_login $os_dir
        chroot $os_dir systemctl enable ssh
    fi

    # fstab
    {
        # /
        uuid=$(lsblk /dev/$xda*2 -no UUID)
        echo "$fstab_line_os" | sed "s/%s/$uuid/"

        # swapfile
        # 官方安装器即使 swapfile 设为 0 也会有这行
        echo "$fstab_line_swapfile"

        # /boot/efi
        if is_efi; then
            uuid=$(lsblk /dev/$xda*1 -no UUID)
            echo "$fstab_line_efi" | sed "s/%s/$uuid/"
        fi
    } >$os_dir/etc/fstab

    # 更新 initrd,官方安装器也有这一步
    # 理论上 /var/tmp 要设置 1777 权限,但飞牛官方安装器安装后不是
    # 需要先创建 /etc/fstab ,否则会有以下警告
    # W: Couldn't identify type of root file system for fsck hook
    mkdir -p $os_dir/var/tmp
    chmod 1777 $os_dir/var/tmp
    chroot $os_dir update-initramfs -u

    # grub
    if is_efi; then
        chroot $os_dir grub-install --efi-directory=/boot/efi
        chroot $os_dir grub-install --efi-directory=/boot/efi --removable
    else
        chroot $os_dir grub-install /dev/$xda
    fi

    # grub 配置
    # 取自 strings trim-install | grep GRUB_DISTRIBUTOR
    sed -i 's/^GRUB_DISTRIBUTOR=.*/GRUB_DISTRIBUTOR="FNOS"/' $os_dir/etc/default/grub

    # grub tty
    ttys_cmdline=$(get_ttys console=)
    echo GRUB_CMDLINE_LINUX=\"\$GRUB_CMDLINE_LINUX $ttys_cmdline\" >$os_dir/etc/default/grub.d/tty.cfg

    chroot $os_dir update-grub

    # 网卡配置
    create_cloud_init_network_config /net.cfg
    create_network_manager_config /net.cfg $os_dir
    rm /net.cfg

    # 修正网卡名
    add_fix_eth_name_systemd_service $os_dir

    # frpc
    add_frpc_systemd_service_if_need $os_dir
}

install_qcow_by_copy() {
    info "Install qcow2 by copy"

    modify_el_ol() {
        info "Modify el ol"
        os_dir=/os

        # resolv.conf
        cp_resolv_conf /os

        # 部分镜像有默认配置,例如 centos
        del_exist_sysconfig_NetworkManager_config /os

        # 删除镜像的默认账户,防止使用默认账户密码登录 ssh
        del_default_user /os

        # selinux kdump
        disable_selinux /os
        disable_kdump /os

        # el7 删除 machine-id 后不会自动重建
        clear_machine_id /os

        # el7 forks 特殊处理
        if is_el7_family /os; then
            # centos 7 eol 换源
            if [ -f /os/etc/yum.repos.d/CentOS-Base.repo ]; then
                # 保持默认的 http 因为自带的 ssl 证书可能过期
                if is_in_china; then
                    mirror=mirror.nju.edu.cn/centos-vault
                else
                    mirror=vault.centos.org
                fi
                sed -Ei -e 's,(mirrorlist=),#\1,' \
                    -e "s,#(baseurl=http://)mirror.centos.org,\1$mirror," /os/etc/yum.repos.d/CentOS-Base.repo
            fi

            # el7 yum 可能会使用 ipv6,即使没有 ipv6 网络
            if [ "$(cat /dev/netconf/*/ipv6_has_internet | sort -u)" = 0 ]; then
                echo 'ip_resolve=4' >>/os/etc/yum.conf
            fi

            # el7 安装 NetworkManager
            # anolis 7 镜像自带 NetworkManager
            if ! [ -f /os/usr/lib/systemd/system/NetworkManager.service ]; then
                chroot_dnf install NetworkManager
            fi
            # 服务不存在时会报错
            chroot /os systemctl disable network 2>/dev/null || true
            chroot /os systemctl enable NetworkManager
        fi

        # firmware + microcode
        if fw_pkgs=$(get_ucode_firmware_pkgs) && [ -n "$fw_pkgs" ]; then
            chroot_dnf install $fw_pkgs
        fi

        # fstab 删除多余分区
        # almalinux/rocky 镜像有 boot 分区
        # oracle 镜像有 swap 分区
        sed -i '/[[:space:]]\/boot[[:space:]]/d' /os/etc/fstab
        sed -i '/[[:space:]]swap[[:space:]]/d' /os/etc/fstab

        # os_part 变量:
        # mapper/vg_main-lv_root
        # mapper/opencloudos-root

        # oracle/opencloudos 系统盘从 lvm 改成 uuid 挂载
        sed -i "s,/dev/$os_part,UUID=$os_part_uuid," /os/etc/fstab
        if ls /os/boot/loader/entries/*.conf 2>/dev/null; then
            # options root=/dev/mapper/opencloudos-root ro console=ttyS0,115200n8 no_timer_check net.ifnames=0 crashkernel=1800M-64G:256M,64G-128G:512M,128G-486G:768M,486G-972G:1024M,972G-:2048M rd.lvm.lv=opencloudos/root rhgb quiet
            sed -i "s,/dev/$os_part,UUID=$os_part_uuid," /os/boot/loader/entries/*.conf
        fi

        # oracle/opencloudos 移除 lvm cmdline
        chroot /os grubby --update-kernel ALL --remove-args "resume rd.lvm.lv"
        # el7 上面那条 grubby 命令不能设置 /etc/default/grub
        sed -i 's/rd.lvm.lv=[^ "]*//g' /os/etc/default/grub

        # fstab 添加 efi 分区
        if is_efi; then
            # centos/oracle 要创建efi条目
            if ! grep /boot/efi /os/etc/fstab; then
                efi_part_uuid=$(lsblk /dev/$xda*1 -no UUID)
                echo "UUID=$efi_part_uuid /boot/efi vfat $efi_mount_opts 0 0" >>/os/etc/fstab
            fi
        else
            # 删除 efi 条目
            sed -i '/[[:space:]]\/boot\/efi[[:space:]]/d' /os/etc/fstab
        fi

        remove_grub_conflict_files() {
            # bios 和 efi 转换前先删除

            # bios转efi出错
            # centos 和 oracle x86_64 镜像只有 bios 镜像,/boot/grub2/grubenv 是真身
            # 安装grub-efi时,grubenv 会改成指向efi分区grubenv软连接
            # 如果安装grub-efi前没有删除原来的grubenv,原来的grubenv将不变,新建的软连接将变成 grubenv.rpmnew
            # 后续grubenv的改动无法同步到efi分区,会造成grub2-setdefault失效

            # efi转bios出错
            # 如果是指向efi目录的软连接(例如el8),先删除它,否则 grub2-install 会报错
            rm -rf /os/boot/grub2/grubenv /os/boot/grub2/grub.cfg
        }

        # openeuler arm 镜像 grub.cfg 在 /os/grub.cfg,可能给外部的 grub 读取,我们用不到
        # centos7 有 grub1 的配置
        rm -rf /os/grub.cfg /os/boot/grub/grub.conf /os/boot/grub/menu.lst

        # 安装引导
        if is_efi; then
            # 只有centos 和 oracle x86_64 镜像没有efi,其他系统镜像已经从efi分区复制了文件
            # openeuler 自带 grub2-efi-ia32,此时安装 grub2-efi 提示已经安装了 grub2-efi-ia32,不会继续安装 grub2-efi-x64

            # 假设极端情况,qcow2 制作时,安装 grub2-efi-x64 时没有挂载 efi 分区,那么 efi 文件会在系统分区下
            # 但我们复制系统分区时挂载了 /boot/efi,因此 efi 文件会正确地复制到 efi 分区
            # 因此无需判断 qcow2 的 efi 是否是独立分区

            # rhel 镜像没有源,直接 yum install 安装可能会报错
            # 因此如果已经安装了要用的包就不再运行 yum install
            need_install=false
            need_remove_grub_conflict_files=false

            [ "$(uname -m)" = x86_64 ] && arch=x64 || arch=aa64
            if ! chroot $os_dir rpm -qi grub2-efi-$arch; then
                need_install=true
                need_remove_grub_conflict_files=true
            elif ! chroot $os_dir rpm -qi shim-$arch || ! chroot $os_dir rpm -qi efibootmgr; then
                need_install=true
            fi

            if $need_install; then
                if $need_remove_grub_conflict_files; then
                    remove_grub_conflict_files
                fi
                chroot_dnf install efibootmgr grub2-efi-$arch shim-$arch
            fi
            # openeuler arm 25.09 云镜像里面的 grubaa64.efi 是用于 mbr 分区表,$root 是 hd0,msdos1
            # 因此要重新下载 $root 是 hd0,gpt1 的 grubaa64.efi
            if $need_reinstall_grub_efi; then
                chroot_dnf reinstall grub2-efi-$arch
            fi
        else
            # bios
            remove_grub_conflict_files
            chroot /os/ grub2-install /dev/$xda
        fi

        # blscfg 启动项
        # rocky/almalinux镜像是独立的boot分区,但我们不是
        # 因此要添加boot目录
        if ls /os/boot/loader/entries/*.conf 2>/dev/null &&
            ! grep -q 'initrd /boot/' /os/boot/loader/entries/*.conf; then

            sed -i -E 's,((linux|initrd) /),\1boot/,g' /os/boot/loader/entries/*.conf
        fi

        # grub-efi-x64 包里面有 /etc/grub2-efi.cfg
        # 指向 /boot/efi/EFI/xxx/grub.cfg 或 /boot/grub2/grub.cfg
        # 指向哪里哪里就是 grub2-mkconfig 应该生成文件的位置
        # grubby 也是靠 /etc/grub2-efi.cfg 定位 grub.cfg 的位置
        # openeuler 24.03 x64 aa64 指向的文件不同
        if is_efi; then
            grub_o_cfg=$(chroot /os readlink -f /etc/grub2-efi.cfg)
        else
            grub_o_cfg=/boot/grub2/grub.cfg
        fi

        # efi 分区 grub.cfg
        # https://github.com/rhinstaller/anaconda/blob/346b932a26a19b339e9073c049b08bdef7f166c3/pyanaconda/modules/storage/bootloader/efi.py#L198
        # https://github.com/rhinstaller/anaconda/commit/15c3b2044367d375db6739e8b8f419ef3e17cae7
        if is_efi && ! echo "$grub_o_cfg" | grep -q '/boot/efi/EFI'; then
            # oracle linux 文件夹是 redhat
            # shellcheck disable=SC2010
            distro_efi=$(cd /os/boot/efi/EFI/ && ls -d -- * | grep -Eiv BOOT)
            cat </os/boot/efi/EFI/$distro_efi/grub.cfg
search --no-floppy --fs-uuid --set=dev $os_part_uuid
set prefix=(\$dev)/boot/grub2
export \$prefix
configfile \$prefix/grub.cfg
EOF
        fi

        # 主 grub.cfg
        if ls /os/boot/loader/entries/*.conf >/dev/null 2>&1 &&
            chroot /os/ grub2-mkconfig --help | grep -q update-bls-cmdline; then
            chroot /os/ grub2-mkconfig -o "$grub_o_cfg" --update-bls-cmdline
        else
            chroot /os/ grub2-mkconfig -o "$grub_o_cfg"
        fi

        # 网络配置
        # el7/8 sysconfig
        # el9 network-manager
        if [ -f $os_dir/etc/sysconfig/network-scripts/ifup-eth ]; then
            # sysconfig
            info 'sysconfig'

            # anolis/openeuler/opencloudos 可能要安装 cloud-init
            # opencloudos 无法使用 chroot $os_dir command -v xxx
            # chroot: failed to run command ‘command’: No such file or directory
            # 注意还要禁用 cloud-init 服务
            if ! is_have_cmd_on_disk $os_dir cloud-init; then
                chroot_dnf install cloud-init
            fi

            # cloud-init 路径
            # /usr/lib/python2.7/site-packages/cloudinit/net/
            # /usr/lib/python3/dist-packages/cloudinit/net/
            # /usr/lib/python3.9/site-packages/cloudinit/net/

            # el7 不认识 static6,但可改成 static,作用相同
            recognize_static6=true
            if ls $os_dir/usr/lib/python*/*-packages/cloudinit/net/sysconfig.py 2>/dev/null &&
                ! grep -q static6 $os_dir/usr/lib/python*/*-packages/cloudinit/net/sysconfig.py; then
                recognize_static6=false
            fi

            # cloud-init 20.1 才支持以下配置
            # https://cloudinit.readthedocs.io/en/20.4/topics/network-config-format-v1.html#subnet-ip
            # https://cloudinit.readthedocs.io/en/21.1/topics/network-config-format-v1.html#subnet-ip
            # ipv6_dhcpv6-stateful: Configure this interface with dhcp6
            # ipv6_dhcpv6-stateless: Configure this interface with SLAAC and DHCP
            # ipv6_slaac: Configure address with SLAAC

            # el7 最新 cloud-init 版本
            # centos 7         19.4-7.0.5.el7_9.6  backport 了 ipv6_xxx
            # openeuler 20.03  19.4-15.oe2003sp4   backport 了 ipv6_xxx
            # anolis 7         19.1.17-1.0.1.an7   没有更新到 centos7 相同版本,也没 backport ipv6_xxx,坑

            # 最好还修改 ifcfg-eth* 的 IPV6_AUTOCONF
            # 但实测 anolis7 cloud-init dhcp6 不会生成 IPV6_AUTOCONF,因此暂时不管
            # https://www.redhat.com/zh/blog/configuring-ipv6-rhel-7-8
            recognize_ipv6_types=true
            if ls -d $os_dir/usr/lib/python*/*-packages/cloudinit/net/ 2>/dev/null &&
                ! grep -qr ipv6_slaac $os_dir/usr/lib/python*/*-packages/cloudinit/net/; then
                recognize_ipv6_types=false
            fi

            # 生成 cloud-init 网络配置
            create_cloud_init_network_config $os_dir/net.cfg "$recognize_static6" "$recognize_ipv6_types"

            # 转换成目标系统的网络配置
            chroot $os_dir cloud-init devel net-convert \
                -p /net.cfg -k yaml -d out -D rhel -O sysconfig
            cp $os_dir/out/etc/sysconfig/network-scripts/ifcfg-eth* $os_dir/etc/sysconfig/network-scripts/

            # 清理
            rm -rf $os_dir/net.cfg $os_dir/out

            # 删除 # Created by cloud-init on instance boot automatically, do not edit.
            # 修正网络配置问题并显示文件
            sed -i -e '/^IPV[46]_FAILURE_FATAL=/d' -e '/^#/d' $os_dir/etc/sysconfig/network-scripts/ifcfg-*
            for file in "$os_dir/etc/sysconfig/network-scripts/ifcfg-"*; do
                if grep -q '^DHCPV6C=yes' "$file"; then
                    sed -i '/^IPV6_AUTOCONF=no/d' "$file"
                fi
                cat -n "$file"
            done
        else
            # Network Manager
            info 'Network Manager'

            create_cloud_init_network_config /net.cfg
            create_network_manager_config /net.cfg "$os_dir"

            # 清理
            rm /net.cfg
        fi

        # 不删除可能网络管理器不会写入dns
        rm_resolv_conf /os
    }

    modify_ubuntu() {
        os_dir=/os
        info "Modify Ubuntu"

        cp_resolv_conf $os_dir

        # 关闭 os prober,因为 os prober 有时很慢
        cp $os_dir/etc/default/grub $os_dir/etc/default/grub.orig
        echo 'GRUB_DISABLE_OS_PROBER=true' >>$os_dir/etc/default/grub

        # 更改源
        if is_in_china; then
            # 22.04 使用 /etc/apt/sources.list
            # 24.04 使用 /etc/apt/sources.list.d/ubuntu.sources
            for file in $os_dir/etc/apt/sources.list $os_dir/etc/apt/sources.list.d/ubuntu.sources; do
                if [ -f $file ]; then
                    # cn.archive.ubuntu.com 不在国内还严重丢包
                    # https://www.itdog.cn/ping/cn.archive.ubuntu.com
                    sed -i 's/archive.ubuntu.com/mirror.nju.edu.cn/' $file # x64
                    sed -i 's/ports.ubuntu.com/mirror.nju.edu.cn/' $file   # arm
                fi
            done
        fi

        # 16.04 arm64 镜像没有 grub 引导文件
        if is_efi && ! [ -d $os_dir/boot/efi/EFI/ubuntu ]; then
            chroot_apt_install $os_dir efibootmgr shim "grub-efi-$(get_axx64)"
            # 创建 ubuntu 文件夹和 grubaa64.efi
            DEBIAN_FRONTEND=noninteractive chroot $os_dir dpkg-reconfigure "grub-efi-$(get_axx64)"

            cat <"$os_dir/boot/efi/EFI/ubuntu/grub.cfg"
search.fs_uuid $os_part_uuid root
set prefix=(\$root)'/boot/grub'
configfile \$prefix/grub.cfg
EOF
        fi

        # 避免 do-release-upgrade 时自动执行 dpkg-reconfigure grub-xx 但是 efi/biosgrub 分区不存在而导致报错
        # shellcheck disable=SC2046
        chroot_apt_remove $os_dir $(is_efi && echo 'grub-pc' || echo 'grub-efi*' 'shim*')
        chroot_apt_autoremove $os_dir

        # 安装 mbr
        if ! is_efi; then
            if false; then
                # debconf-show grub-pc
                # 每次开机硬盘名字可能不一样,但是 debian netboot 安装后也是设置了 grub-pc/install_devices
                echo grub-pc grub-pc/install_devices multiselect /dev/$xda | chroot $os_dir debconf-set-selections # 22.04
                echo grub-pc grub-pc/cloud_style_installation boolean true | chroot $os_dir debconf-set-selections # 24.04
                chroot $os_dir dpkg-reconfigure -f noninteractive grub-pc
            else
                chroot $os_dir grub-install /dev/$xda
            fi
        fi

        # 自带内核:
        # 常规版本             generic
        # minimal 20.04/22.04 kvm      # 后台 vnc 无显示
        # minimal 24.04       virtual

        # debian cloud 内核不支持 ahci,ubuntu virtual 支持

        # 标记所有内核为自动安装
        # 注意排除 linux-base
        # 返回值始终为 0
        pkgs=$(chroot $os_dir apt-mark showmanual \
            linux-generic linux-virtual linux-kvm \
            linux-image* linux-headers*)
        chroot $os_dir apt-mark auto $pkgs

        # 安装最佳内核
        flavor=$(get_ubuntu_kernel_flavor)
        echo "Use kernel flavor: $flavor"

        # 题外话
        # 如果某个包是 auto 状态且有更新
        # 则 apt install PKG 只会进行更新,不会将包设置成 manual
        # 需要再次运行 apt install PKG 才会将包设置成 manual

        # 该方法包含了 apt-mark manual
        chroot_apt_install $os_dir "linux-image-$flavor"

        # 使用 autoremove 删除多余内核
        chroot_apt_autoremove $os_dir

        # 安装固件+微码
        if fw_pkgs=$(get_ucode_firmware_pkgs) && [ -n "$fw_pkgs" ]; then
            chroot_apt_install $os_dir $fw_pkgs
        fi

        # 网络配置
        # 18.04+ netplan
        if is_have_cmd_on_disk $os_dir netplan; then
            # 避免删除 cloud-init 后,minimal 镜像的 netplan.io 被 autoremove
            chroot $os_dir apt-mark manual netplan.io

            # 生成 cloud-init 网络配置
            create_cloud_init_network_config $os_dir/net.cfg

            # ubuntu 18.04 cloud-init 版本 23.1.2,因此不用处理 onlink

            # 如果不是输出到 / 则不会生成 50-cloud-init.yaml
            # 注意比较多了什么东西
            if false; then
                chroot $os_dir cloud-init devel net-convert \
                    -p /net.cfg -k yaml -d /out -D ubuntu -O netplan
                sed -Ei "/^[[:space:]]+set-name:/d" $os_dir/out/etc/netplan/50-cloud-init.yaml
                cp $os_dir/out/etc/netplan/50-cloud-init.yaml $os_dir/etc/netplan/

                # 清理
                rm -rf $os_dir/net.cfg $os_dir/out
            else
                chroot $os_dir cloud-init devel net-convert \
                    -p /net.cfg -k yaml -d / -D ubuntu -O netplan
                sed -Ei "/^[[:space:]]+set-name:/d" $os_dir/etc/netplan/50-cloud-init.yaml

                # 清理
                rm -rf $os_dir/net.cfg
            fi
        else
            # 避免删除 cloud-init 后 ifupdown 被 autoremove
            chroot $os_dir apt-mark manual ifupdown

            # 16.04 镜像用 ifupdown/networking 管理网络
            # 要安装 resolveconf,不然 /etc/resolv.conf 为空
            chroot_apt_install $os_dir resolvconf
            ln -sf /run/resolvconf/resolv.conf $os_dir/etc/resolv.conf.orig

            create_ifupdown_config $os_dir/etc/network/interfaces
        fi

        # 自带的 60-cloudimg-settings.conf 禁止了 PasswordAuthentication
        file=$os_dir/etc/ssh/sshd_config.d/60-cloudimg-settings.conf
        if [ -f $file ]; then
            sed -i '/^PasswordAuthentication/d' $file
            if [ -z "$(cat $file)" ]; then
                rm -f $file
            fi
        fi

        # 更改 efi 目录的 grub.cfg 写死的 fsuuid
        # 因为 24.04 fsuuid 对应 boot 分区
        efi_grub_cfg=$os_dir/boot/efi/EFI/ubuntu/grub.cfg
        if is_efi; then
            os_uuid=$(lsblk -rno UUID /dev/$xda*2)
            sed -Ei "s|[0-9a-f-]{36}|$os_uuid|i" $efi_grub_cfg

            # 24.04 移除 boot 分区后,需要添加 /boot 路径
            if grep "'/grub'" $efi_grub_cfg; then
                sed -i "s|'/grub'|'/boot/grub'|" $efi_grub_cfg
            fi
        fi

        # 处理 40-force-partuuid.cfg
        force_partuuid_cfg=$os_dir/etc/default/grub.d/40-force-partuuid.cfg
        if [ -e $force_partuuid_cfg ]; then
            if is_virt; then
                # 更改写死的 partuuid
                os_part_uuid=$(lsblk -rno PARTUUID /dev/$xda*2)
                sed -i "s/^GRUB_FORCE_PARTUUID=.*/GRUB_FORCE_PARTUUID=$os_part_uuid/" $force_partuuid_cfg
            else
                # 独服不应该使用 initrdless boot
                sed -i "/^GRUB_FORCE_PARTUUID=/d" $force_partuuid_cfg
            fi
        fi

        # 要重新生成 grub.cfg,因为
        # 1 我们删除了 boot 分区
        # 2 改动了 /etc/default/grub.d/40-force-partuuid.cfg
        chroot $os_dir update-grub

        # 还原 grub 配置(os prober)
        mv $os_dir/etc/default/grub.orig $os_dir/etc/default/grub

        # fstab
        # 24.04 镜像有boot分区,但我们不需要
        sed -i '/[[:space:]]\/boot[[:space:]]/d' $os_dir/etc/fstab
        if ! is_efi; then
            # bios 删除 efi 条目
            sed -i '/[[:space:]]\/boot\/efi[[:space:]]/d' $os_dir/etc/fstab
        fi

        restore_resolv_conf $os_dir
    }

    efi_mount_opts=$(
        case "$distro" in
        ubuntu) echo "umask=0077" ;;
        *) echo "defaults,uid=0,gid=0,umask=077,shortname=winnt" ;;
        esac
    )

    # yum/apt 安装软件时需要的内存总大小
    need_ram=$(
        case "$distro" in
        ubuntu) echo 1024 ;;
        *) echo 2048 ;;
        esac
    )

    connect_qcow

    # 镜像分区格式
    # centos/rocky/almalinux/rhel: xfs
    # oracle x86_64:          lvm + xfs
    # oracle aarch64 cloud:   xfs
    # alibaba cloud linux 3:  ext4

    is_lvm_image=false
    if lsblk -f /dev/nbd0p* | grep LVM2_member; then
        is_lvm_image=true
        apk add lvm2
        lvscan
        vg=$(pvs | grep /dev/nbd0p | awk '{print $2}')
        lvchange -ay "$vg"
    fi

    mount_nouuid() {
        part_fstype=
        for arg in "$@"; do
            case "$arg" in
            /dev/*)
                part_fstype=$(lsblk -no FSTYPE "$arg")
                break
                ;;
            esac
        done

        case "$part_fstype" in
        xfs) mount -o nouuid "$@" ;;
        *) mount "$@" ;;
        esac
    }

    # 可以直接选择最后一个分区为系统分区?
    # almalinux9 boot 分区的类型不是规定的 uuid
    # openeuler boot 分区是 vfat 格式
    # openeuler arm 25.09 是 mbr 分区表, efi boot 是同一个分区,vfat 格式

    info "qcow2 Partitions check"

    # 检测分区表类型
    partition_table_format=$(get_partition_table_format /dev/nbd0)
    need_reinstall_grub_efi=false
    if is_efi && [ "$partition_table_format" = "msdos" ]; then
        need_reinstall_grub_efi=true
    fi

    # 通过检测文件判断是什么分区
    os_part='' boot_part='' efi_part=''
    mkdir -p /nbd-test
    for part in $(lsblk /dev/nbd0p* --sort SIZE -no NAME,FSTYPE |
        grep -E ' (ext4|xfs|fat|vfat)$' | awk '{print $1}' | tac); do
        mapper_part=$part
        if $is_lvm_image && [ -e /dev/mapper/$part ]; then
            mapper_part=mapper/$part
        fi

        if mount_nouuid -o ro /dev/$mapper_part /nbd-test; then
            if { ls /nbd-test/etc/os-release || ls /nbd-test/*/etc/os-release; } 2>/dev/null; then
                os_part=$mapper_part
            fi
            # shellcheck disable=SC2010
            # 当 boot 作为独立分区时,vmlinuz 等文件在根目录
            # 当 boot 不是独立分区时,vmlinuz 等文件在 /boot 目录
            if ls /nbd-test/ /nbd-test/boot/ 2>/dev/null | grep -Ei '^(vmlinuz|initrd|initramfs)'; then
                boot_part=$mapper_part
            fi
            # mbr + efi 引导 ,分区表没有 esp guid
            # 因此需要用 efi 文件判断是否 efi 分区
            # efi 文件可能在 efi 目录的子目录,子目录层数不定
            if find /nbd-test/ -type f -ipath '/nbd-test/EFI/*.efi' 2>/dev/null | grep .; then
                efi_part=$mapper_part
            fi
            umount /nbd-test
        fi
    done

    info "qcow2 Partitions"
    lsblk -f /dev/nbd0 -o +PARTTYPE
    # 显示 OS/EFI/Boot 文件在哪个分区
    echo "---"
    echo "Table:     $partition_table_format"
    echo "Part OS:   $os_part"
    echo "Part EFI:  $efi_part"
    echo "Part Boot: $boot_part"
    echo "---"

    # 分区寻找方式
    # 系统/分区          cmdline:root  fstab:efi
    # rocky             LABEL=rocky   LABEL=EFI
    # ubuntu            PARTUUID      LABEL=UEFI
    # 其他el/ol         UUID           UUID

    IFS=, read -r os_part_uuid os_part_label os_part_fstype \
        < <(lsblk /dev/$os_part -rno UUID,LABEL,FSTYPE | tr ' ' ,)

    if [ -n "$efi_part" ]; then
        IFS=, read -r efi_part_uuid efi_part_label \
            < <(lsblk /dev/$efi_part -rno UUID,LABEL | tr ' ' ,)
    fi

    mkdir -p /nbd /nbd-boot /nbd-efi

    # 使用目标系统的格式化程序
    # centos8 如果用alpine格式化xfs,grub2-mkconfig和grub2里面都无法识别xfs分区
    mount_nouuid /dev/$os_part /nbd/
    mount_pseudo_fs /nbd/
    case "$os_part_fstype" in
    ext4) chroot /nbd mkfs.ext4 -F -L "$os_part_label" -U "$os_part_uuid" /dev/$xda*2 ;;
    xfs) chroot /nbd mkfs.xfs -f -L "$os_part_label" -m uuid=$os_part_uuid /dev/$xda*2 ;;
    esac
    umount -R /nbd/

    # TODO: ubuntu 镜像缺少 mkfs.fat/vfat/dosfstools? initrd 不需要检查fs完整性?

    # 创建并挂载 /os
    mkdir -p /os
    mount -o noatime /dev/$xda*2 /os/

    # 如果是 efi 则创建 /os/boot/efi
    # 如果镜像有 efi 分区也创建 /os/boot/efi,用于复制 efi 分区的文件
    if is_efi || [ -n "$efi_part" ]; then
        mkdir -p /os/boot/efi/

        # 挂载 /os/boot/efi
        # 预先挂载 /os/boot/efi 因为可能 boot 和 efi 在同一个分区(openeuler 24.03 arm)
        # 复制 boot 时可以会复制 efi 的文件
        if is_efi; then
            mount -o $efi_mount_opts /dev/$xda*1 /os/boot/efi/
        fi
    fi

    # 复制系统分区
    echo Copying os partition...
    mount_nouuid -o ro /dev/$os_part /nbd/
    cp -a /nbd/* /os/
    umount /nbd/

    # 复制独立的boot分区,如果有
    if [ -n "$boot_part" ] && ! [ "$boot_part" = "$os_part" ]; then
        echo Copying boot partition...
        mount_nouuid -o ro /dev/$boot_part /nbd-boot/
        cp -a /nbd-boot/* /os/boot/
        umount /nbd-boot/
    fi

    # 复制独立的efi分区,如果有
    # 如果 efi 和 boot 是同一个分区,则复制 boot 分区时已经复制了 efi 分区的文件
    if [ -n "$efi_part" ] && ! [ "$efi_part" = "$os_part" ] && ! [ "$efi_part" = "$boot_part" ]; then
        echo Copying efi partition...
        mount -o ro /dev/$efi_part /nbd-efi/
        cp -a /nbd-efi/* /os/boot/efi/
        umount /nbd-efi/
    fi

    # 断开 qcow 并删除 qemu-img
    info "Disconnecting qcow2"
    if is_have_cmd vgchange; then
        vgchange -an
        apk del lvm2
    fi
    disconnect_qcow
    apk del qemu-img

    # 取消挂载硬盘
    info "Unmounting disk"
    if is_efi; then
        umount /os/boot/efi/
    fi
    umount /os/
    umount /installer/

    # 如果镜像有独立的efi分区(包括efi+boot在同一个分区),复制其uuid
    # 如果有相同uuid的fat分区,则无法挂载
    # 所以要先复制efi分区,断开nbd再复制uuid
    # 复制uuid前要取消挂载硬盘 efi 分区
    if is_efi && [ -n "$efi_part_uuid" ] && ! [ "$efi_part" = "$os_part" ]; then
        info "Copy efi partition uuid"
        apk add mtools
        mlabel -N "$(echo $efi_part_uuid | sed 's/-//')" -i /dev/$xda*1 ::$efi_part_label
        apk del mtools
        update_part
    fi

    # 删除 installer 分区并扩容
    info "Delete installer partition"
    apk add parted
    parted /dev/$xda -s -- rm 3
    update_part
    resize_after_install_cloud_image

    # 重新挂载 /os /boot/efi
    info "Re-mount disk"
    mount -o noatime /dev/$xda*2 /os/
    if is_efi; then
        mount -o $efi_mount_opts /dev/$xda*1 /os/boot/efi/
    fi

    # 创建 swap
    create_swap_if_ram_less_than $need_ram /os/swapfile

    # 挂载伪文件系统
    mount_pseudo_fs /os/

    case "$distro" in
    ubuntu) modify_ubuntu ;;
    *) modify_el_ol ;;
    esac

    # 基本配置
    basic_init /os

    # 最后才删除 cloud-init
    # 因为生成 netplan/sysconfig 网络配置要用目标系统的 cloud-init
    remove_or_disable_cloud_init /os

    # 删除 swapfile
    swapoff -a
    rm -f /os/swapfile
}

get_partition_table_format() {
    apk add parted
    parted "$1" -s print | grep 'Partition Table:' | awk '{print $NF}'
}

dd_qcow() {
    info "DD qcow2"

    if true; then
        connect_qcow

        partition_table_format=$(get_partition_table_format /dev/nbd0)
        orig_nbd_virtual_size=$(get_disk_size /dev/nbd0)

        # 检查最后一个分区是否是 btrfs
        # 即使awk结果为空,返回值也是0,加上 grep . 检查是否结果为空
        if part_num=$(parted /dev/nbd0 -s print | awk NF | tail -1 | grep btrfs | awk '{print $1}' | grep .); then
            apk add btrfs-progs
            mkdir -p /mnt/btrfs
            mount /dev/nbd0p$part_num /mnt/btrfs

            # 回收空数据块
            btrfs device usage /mnt/btrfs
            btrfs balance start -dusage=0 /mnt/btrfs
            btrfs device usage /mnt/btrfs

            # 计算可以缩小的空间
            free_bytes=$(btrfs device usage /mnt/btrfs -b | grep Unallocated: | awk '{print $2}')
            reserve_bytes=$((100 * 1024 * 1024)) # 预留 100M 可用空间
            skrink_bytes=$((free_bytes - reserve_bytes))

            if [ $skrink_bytes -gt 0 ]; then
                # 缩小文件系统
                btrfs filesystem resize -$skrink_bytes /mnt/btrfs
                # 缩小分区
                part_start=$(parted /dev/nbd0 -s 'unit b print' | awk "\$1==$part_num {print \$2}" | sed 's/B//')
                part_size=$(btrfs filesystem usage /mnt/btrfs -b | grep 'Device size:' | awk '{print $3}')
                part_end=$((part_start + part_size - 1))
                umount /mnt/btrfs
                printf "yes" | parted /dev/nbd0 resizepart $part_num ${part_end}B ---pretend-input-tty

                # 缩小 qcow2
                disconnect_qcow
                qemu-img resize --shrink $qcow_file $((part_end + 1))

                # 重新连接
                connect_qcow
            else
                umount /mnt/btrfs
            fi
        fi

        # 显示分区
        lsblk -o NAME,SIZE,FSTYPE,LABEL /dev/nbd0

        # 将前1M dd到内存
        dd if=/dev/nbd0 of=/first-1M bs=1M count=1

        # 将1M之后 dd到硬盘
        # shellcheck disable=SC2194
        case 3 in
        1)
            # BusyBox dd
            dd if=/dev/nbd0 of=/dev/$xda bs=1M skip=1 seek=1
            ;;
        2)
            # 用原版 dd status=progress,但没有进度和剩余时间
            apk add coreutils
            dd if=/dev/nbd0 of=/dev/$xda bs=1M skip=1 seek=1 status=progress
            ;;
        3)
            # 用 pv
            apk add pv
            echo "Start DD Cloud Image..."
            pv -f /dev/nbd0 | dd of=/dev/$xda bs=1M skip=1 seek=1 iflag=fullblock
            ;;
        esac

        disconnect_qcow
    else
        # 将前1M dd到内存,将1M之后 dd到硬盘
        qemu-img dd if=$qcow_file of=/first-1M bs=1M count=1
        qemu-img dd if=$qcow_file of=/dev/disk/by-label/os bs=1M skip=1
    fi

    # 已 dd 并断开连接 qcow,可删除 qemu-img
    apk del qemu-img

    # 将前1M从内存 dd 到硬盘
    umount /installer/
    dd if=/first-1M of=/dev/$xda
    rm -f /first-1M

    # gpt 分区表开头记录了备份分区表的位置
    # 如果 qcow2 虚拟容量 大于 实际硬盘容量
    # 备份分区表的位置 将超出实际硬盘容量的大小
    # partprobe 会报错
    # Error: Invalid argument during seek for read on /dev/vda
    # parted 也无法正常工作
    # 需要提前修复分区表

    # 目前只有这个例子,因为其他 qcow2 虚拟容量最多 5g,是设定支持的容量
    # openSUSE-Leap-15.5-Minimal-VM.x86_64-kvm-and-xen.qcow2 容量是 25g
    # 缩小 btrfs 分区后 dd 到 10g 的机器上
    # 备份分区表的位置是 25g
    # 需要修复到 10g 的位置上
    # 否则 partprobe parted 都无法正常工作

    # 仅这种情况才用 sgdisk 修复
    if [ "$partition_table_format" = gpt ] &&
        [ "$orig_nbd_virtual_size" -gt "$(get_disk_size /dev/$xda)" ]; then
        fix_gpt_backup_partition_table_by_sgdisk
    fi
    update_part
}

fix_gpt_backup_partition_table_by_sgdisk() {
    # 当备份分区表超出实际硬盘容量时,只能用 sgdisk 修复分区表
    # 应用场景:镜像大小超出硬盘实际硬盘,但缩小分区后不超出实际硬盘容量,可以顺利 DD
    # 例子 openSUSE-Leap-15.5-Minimal-VM.x86_64-kvm-and-xen.qcow2

    # parted 无法修复
    # parted /dev/$xda -f -s print

    # fdisk/sfdisk 显示主分区表损坏
    # echo write | sfdisk /dev/$xda
    # GPT PMBR size mismatch (50331647 != 20971519) will be corrected by write.
    # The primary GPT table is corrupt, but the backup appears OK, so that will be used.

    # 除此之外的场景应该用 parted 来修复

    apk add sgdisk

    # 两种方法都可以,但都不会修复备份分区表的 GUID
    # 此时 sgdisk -v /dev/vda 会提示主副分区表 guid 不相同
    # localhost:~# sgdisk -v /dev/$xda
    # Problem: main header's disk GUID (A24485F3-2C02-43BD-BF4E-F52E42B00DEA) doesn't
    # match the backup GPT header's disk GUID (ADAF57BC-B4F5-4E04-BCBA-BDDCD796C388)
    # You should use the 'b' or 'd' option on the recovery & transformation menu to
    # select one or the other header.
    if false; then
        sgdisk --backup /gpt-partition-table /dev/$xda
        sgdisk --load-backup /gpt-partition-table /dev/$xda
    else
        sgdisk --move-second-header /dev/$xda
    fi

    # 因此需要运行一次设置 guid
    if new_guid=$(sgdisk -v /dev/$xda | grep GUID | head -1 | grep -Eo '[0-9A-F-]{36}'); then
        sgdisk --disk-guid $new_guid /dev/$xda
    fi

    update_part

    apk del sgdisk
}

# 适用于 DD 后修复 gpt 备份分区表
fix_gpt_backup_partition_table_by_parted() {
    apk add parted
    parted /dev/$xda -f -s print
    update_part
}

resize_after_install_cloud_image() {
    # 提前扩容
    # 1 修复 vultr 512m debian 11 generic/genericcloud 首次启动 kernel panic
    # 2 防止 gentoo 云镜像 websync 时空间不足
    info "Resize after dd"
    lsblk -f /dev/$xda

    # 打印分区表,并自动修复备份分区表
    fix_gpt_backup_partition_table_by_parted

    disk_size=$(get_disk_size /dev/$xda)
    disk_end=$((disk_size - 1))

    # 不能漏掉最后的 _ ,否则第6部分都划到给 last_part_fs
    IFS=: read -r last_part_num _ last_part_end _ last_part_fs _ \
        < <(parted -msf /dev/$xda 'unit b print' | tail -1)
    last_part_end=$(echo $last_part_end | sed 's/B//')

    if [ $((disk_end - last_part_end)) -ge 0 ]; then
        printf "yes" | parted /dev/$xda resizepart $last_part_num 100% ---pretend-input-tty
        update_part

        mkdir -p /os

        # lvm ?
        # 用 cloud-utils-growpart?
        case "$last_part_fs" in
        ext4)
            # debian ci
            apk add e2fsprogs-extra
            e2fsck -p -f /dev/$xda*$last_part_num
            resize2fs /dev/$xda*$last_part_num
            apk del e2fsprogs-extra
            ;;
        xfs)
            # opensuse ci
            apk add xfsprogs-extra
            mount /dev/$xda*$last_part_num /os
            xfs_growfs /dev/$xda*$last_part_num
            umount /os
            apk del xfsprogs-extra
            ;;
        btrfs)
            # fedora ci
            apk add btrfs-progs
            mount /dev/$xda*$last_part_num /os
            btrfs filesystem resize max /os
            umount /os
            apk del btrfs-progs
            ;;
        ntfs)
            # windows dd
            apk add ntfs-3g-progs
            echo y | ntfsresize /dev/$xda*$last_part_num
            ntfsfix -d /dev/$xda*$last_part_num
            apk del ntfs-3g-progs
            ;;
        esac
        update_part
        parted /dev/$xda -s print
    fi
}

mount_part_basic_layout() {
    os_dir=$1
    efi_dir=$2

    if is_efi || is_xda_gt_2t; then
        os_part_num=2
    else
        os_part_num=1
    fi

    # 挂载系统分区
    mkdir -p $os_dir
    mount -t ext4 /dev/${xda}*${os_part_num} $os_dir

    # 挂载 efi 分区
    if is_efi; then
        mkdir -p $efi_dir
        mount -t vfat -o umask=077 /dev/${xda}*1 $efi_dir
    fi
}

mount_part_for_iso_installer() {
    info "Mount part for iso installer"

    if [ "$distro" = windows ]; then
        mount_args="-t ntfs3 -o nocase"
    else
        mount_args=
    fi

    # 挂载主分区
    mkdir -p /os
    mount $mount_args /dev/disk/by-label/os /os

    # 挂载其他分区
    if is_efi; then
        mkdir -p /os/boot/efi
        mount /dev/disk/by-label/efi /os/boot/efi
    fi
    mkdir -p /os/installer
    mount $mount_args /dev/disk/by-label/installer /os/installer
}

get_dns_list_for_win() {
    if dns_list=$(get_current_dns $1); then
        i=0
        for dns in $dns_list; do
            i=$((i + 1))
            echo "set ipv${1}_dns$i=$dns"
        done
    fi
}

create_win_set_netconf_script() {
    target=$1
    info "Create win netconf script"

    if is_staticv4 || is_staticv6 || is_need_manual_set_dnsv6; then
        get_netconf_to mac_addr
        echo "set mac_addr=$mac_addr" >$target

        # 生成静态 ipv4 配置
        if is_staticv4; then
            get_netconf_to ipv4_addr
            get_netconf_to ipv4_gateway
            cat <>$target
set ipv4_addr=$ipv4_addr
set ipv4_gateway=$ipv4_gateway
$(get_dns_list_for_win 4)
EOF
        fi

        # 生成静态 ipv6 配置
        if is_staticv6; then
            get_netconf_to ipv6_addr
            get_netconf_to ipv6_gateway
            cat <>$target
set ipv6_addr=$ipv6_addr
set ipv6_gateway=$ipv6_gateway
EOF
        fi

        # 有 ipv6 但需设置 dns 的情况
        if is_need_manual_set_dnsv6; then
            cat <>$target
$(get_dns_list_for_win 6)
EOF
        fi

        cat -n $target
    fi

    # 脚本还有关闭ipv6隐私id的功能,所以不能省略
    # 合并脚本
    wget $confhome/windows-set-netconf.bat -O- >>$target
    unix2dos $target
}

create_win_change_rdp_port_script() {
    target=$1
    rdp_port=$2

    info "Create win change rdp port script"

    echo "set RdpPort=$rdp_port" >$target
    wget $confhome/windows-change-rdp-port.bat -O- >>$target
    unix2dos $target
}

# virt-what 要用最新版
# vultr 1G High Frequency LAX 实际上是 kvm
# debian 11 virt-what 1.19 显示为 hyperv qemu
# debian 11 systemd-detect-virt 显示为 microsoft
# alpine virt-what 1.25 显示为 kvm
# 所以不要在原系统上判断具体虚拟化环境

# lscpu 也可查看虚拟化环境,但 alpine on lightsail 运行结果为 Microsoft
# 猜测 lscpu 只参考了 cpuid 没参考 dmi
# virt-what 可能会输出多行结果,因此用 grep

get_aws_repo() {
    if is_in_china >&2; then
        echo https://s3.cn-north-1.amazonaws.com.cn/ec2-windows-drivers-downloads-cn
    else
        echo https://s3.amazonaws.com/ec2-windows-drivers-downloads
    fi
}

get_client_name_by_build_ver() {
    build_ver=$1

    if [ "$build_ver" -ge 22000 ]; then
        echo 11
    elif [ "$build_ver" -ge 10240 ]; then
        echo 10
    elif [ "$build_ver" -ge 9600 ]; then
        echo 8.1
    elif [ "$build_ver" -ge 9200 ]; then
        echo 8
    elif [ "$build_ver" -ge 7600 ]; then
        echo 7
    elif [ "$build_ver" -ge 6000 ]; then
        echo vista
    else
        error_and_exit "Unknown Build Version: $build_ver"
    fi
}

# 将 AC/SAC 版本号 转换为 LTSC 版本号
# 用于查找驱动
get_server_name_by_build_ver() {
    build_ver=$1

    if [ "$build_ver" -ge 26100 ]; then
        echo 2025
    elif [ "$build_ver" -ge 20348 ]; then
        echo 2022
    elif [ "$build_ver" -ge 17763 ]; then
        echo 2019
    elif [ "$build_ver" -ge 14393 ]; then
        echo 2016
    elif [ "$build_ver" -ge 9600 ]; then
        echo 2012 r2
    elif [ "$build_ver" -ge 9200 ]; then
        echo 2012
    elif [ "$build_ver" -ge 7600 ]; then
        echo 2008 r2
    elif [ "$build_ver" -ge 6001 ]; then
        echo 2008
    else
        error_and_exit "Unknown Build Version: $build_ver"
    fi
}

is_nt_ver_ge() {
    local orig sorted
    orig=$(printf '%s\n' "$1" "$nt_ver")
    sorted=$(echo "$orig" | sort -V)
    [ "$orig" = "$sorted" ]
}

get_cloud_vendor() {
    # busybox blkid 不显示 sr0 的 UUID
    apk add lsblk

    # http://git.annexia.org/?p=virt-what.git;a=blob;f=virt-what.in;hb=HEAD
    # virt-what 可识别厂商 aws google_cloud alibaba_cloud alibaba_cloud-ebm
    if is_dmi_contains "Amazon EC2" || is_virt_contains aws; then
        echo aws
    elif is_dmi_contains "Google Compute Engine" || is_dmi_contains "GoogleCloud" || is_virt_contains google_cloud; then
        echo gcp
    elif is_dmi_contains "OracleCloud"; then
        echo oracle
    elif is_dmi_contains "7783-7084-3265-9085-8269-3286-77"; then
        echo azure
    elif lsblk -o UUID,LABEL | grep -i 9796-932E | grep -iq config-2; then
        echo ibm
    elif is_dmi_contains 'Huawei Cloud'; then
        echo huawei
    elif is_dmi_contains 'Alibaba Cloud'; then
        echo aliyun
    elif is_dmi_contains 'Tencent Cloud'; then
        echo qcloud
    fi
}

get_filesize_mb() {
    du -m "$1" | awk '{print $1}'
}

is_absolute_path() {
    # 检查路径是否以/开头
    # 注意语法和 bash 不同
    [[ "$1" = "/*" ]]
}

# 注意使用方法是 list=$(list_add "$list" "$item_to_add")
list_add() {
    local list=$1
    local item_to_add=$2
    if [ -n "$list" ]; then
        echo "$list"
    fi
    echo "$item_to_add"
}

is_list_has() {
    local list=$1
    local item=$2
    echo "$list" | grep -qFx "$item"
}

get_windows_type_from_windows_drive() {
    local os_dir=$1

    apk add hivex
    software_hive=$(find_file_ignore_case $os_dir/Windows/System32/config/SOFTWARE)
    system_hive=$(find_file_ignore_case $os_dir/Windows/System32/config/SYSTEM)
    installation_type=$(hivexget $software_hive '\Microsoft\Windows NT\CurrentVersion' InstallationType 2>/dev/null || true)
    product_type=$(hivexget $system_hive '\ControlSet001\Control\ProductOptions' ProductType 2>/dev/null || true)
    apk del hivex

    # 根据 win11 multi-session 的情况
    # InstallationType 比 ProductType 准确

    # Vista wim 和注册表都没有 InstallationType
    case "$installation_type" in
    Client | Embedded) echo client ;;
    Server | 'Server Core') echo server ;;
    *) case "$product_type" in
        WinNT) echo client ;;
        ServerNT) echo server ;;
        *) error_and_exit "Unknown Windows Type" ;;
        esac ;;
    esac
}

get_windows_arch_from_windows_drive() {
    local os_dir=$1

    apk add hivex
    hive=$(find_file_ignore_case $os_dir/Windows/System32/config/SYSTEM)
    # 没有 CurrentControlSet
    hivexget $hive 'ControlSet001\Control\Session Manager\Environment' PROCESSOR_ARCHITECTURE
    apk del hivex
}

install_windows() {
    get_wim_prop() {
        wim=$1
        property=$2

        wiminfo "$wim" | grep -i "^$property:" | cut -d: -f2- | trim
    }

    get_image_prop() {
        wim=$1
        index=$2
        property=$3

        wiminfo "$wim" "$index" | grep -i "^$property:" | cut -d: -f2- | trim
    }

    info "Process windows iso"
    mkdir -p /iso /wim

    # find_file_ignore_case 也在这个文件里面
    # shellcheck disable=SC1090
    . <(wget -O- $confhome/windows-driver-utils.sh)

    apk add wimlib

    download $iso /os/windows.iso
    mount -o ro /os/windows.iso /iso

    sources_boot_wim=$(
        cd /iso
        find_file_ignore_case sources/boot.wim 2>/dev/null ||
            error_and_exit "can't find boot.wim"
    )

    # 一般镜像是 install.wim
    # en_server_install_disc_windows_home_server_2011_x64_dvd_658487.iso 是 Install.wim
    # en_windows_vista_sp2_with_update_6003.23713_aio_7in1_x64_v26.01.13_by_adguard.iso 是 swm
    source_install_wim=$(
        cd /iso
        {
            find_file_ignore_case sources/install.wim ||
                find_file_ignore_case sources/install.esd ||
                find_file_ignore_case sources/install.swm
        } 2>/dev/null || error_and_exit "can't find install.wim, install.esd or install.swm"
    )

    is_swm=false
    if [[ $(echo "$source_install_wim" | to_lower) = '*.swm' ]]; then
        is_swm=true
        swm_ref=$(
            IFS=. read -r name ext < <(basename "$source_install_wim")
            echo "$name*.$ext"
        )
    fi

    # 防止用了不兼容架构的 iso
    boot_index=$(get_wim_prop "/iso/$sources_boot_wim" 'Boot Index')
    arch_wim=$(get_image_prop "/iso/$sources_boot_wim" "$boot_index" 'Architecture' | to_lower)
    if ! {
        { [ "$(uname -m)" = "x86_64" ] && [ "$arch_wim" = x86_64 ]; } ||
            { [ "$(uname -m)" = "x86_64" ] && [ "$arch_wim" = x86 ]; } ||
            { [ "$(uname -m)" = "aarch64" ] && [ "$arch_wim" = arm64 ]; }
    }; then
        error_and_exit "The machine is $(uname -m), but the iso is $arch_wim."
    fi

    # efi 机器不能安装 32 位 windows
    if is_efi && [ "$arch_wim" = x86 ]; then
        error_and_exit "EFI machine can't install 32-bit Windows."
    fi

    iso_install_wim=/iso/$source_install_wim
    install_wim=/os/installer/$source_install_wim

    # 匹配映像版本
    # 需要整行匹配,因为要区分 Windows 10 Pro 和 Windows 10 Pro for Workstations
    image_count=$(wiminfo $iso_install_wim | grep "^Image Count:" | cut -d: -f2 | trim)
    all_image_names=$(wiminfo $iso_install_wim | grep ^Name: | sed 's/^Name: *//')
    info "Images Count: $image_count"
    echo "$all_image_names"
    echo

    if [ "$image_count" = 1 ]; then
        # 只有一个版本就用那个版本
        image_name=$all_image_names
        image_index=1
    else
        while true; do
            # 匹配成功
            # 改成正确的大小写
            if matched_image_name=$(printf '%s\n' "$all_image_names" | grep -Fix "$image_name"); then
                image_name=$matched_image_name
                image_index=$(wiminfo "$iso_install_wim" "$image_name" | grep 'Index:' | awk '{print $NF}')
                break
            fi

            # 匹配失败
            file=/image-name
            error "Invalid image name: $image_name"
            echo "Choose a correct image name by one of follow command in ssh to continue:"
            while read -r line; do
                echo "  echo '$line' >$file"
            done < <(echo "$all_image_names")

            # sleep 直到有输入
            true >$file
            while ! { [ -s $file ] && image_name=$(cat $file) && [ -n "$image_name" ]; }; do
                sleep 1
            done
        done
    fi

    get_selected_image_prop() {
        get_image_prop "$iso_install_wim" "$image_index" "$1"
    }

    # 多会话的信息来自注册表,因为没有官方 iso

    # Installation Type:
    # https://github.com/search?q=InstallationType+Client+Embedded+Server+Core&type=code
    # - Client      (普通 windows)
    # - Server      (windows server 带桌面体验)
    # - Server Core (windows server 不带桌面体验)
    # - Embedded    (WES7 / Thin PC)
    # - Client      (windows 10/11 enterprise 多会话)

    # Product Type:
    # https://cloud.tencent.com/developer/article/2465206
    # https://learn.microsoft.com/en-us/azure/virtual-desktop/windows-multisession-faq#why-does-my-application-report-windows-enterprise-multi-session-as-a-server-operating-system
    # - WinNT    (普通 windows)
    # - ServerNT (windows server 带桌面体验)
    # - ServerNT (windows server 不带桌面体验)
    # - WinNT    (WES7 / Thin PC)
    # - ServerNT (windows 10/11 enterprise 多会话)

    # Product Suite:
    # https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/exinit/productsuite.htm
    # - Terminal Server (普通 windows)
    # - Enterprise      (windows server 2025 带桌面体验)
    # - Enterprise      (windows server 2025 不带桌面体验)
    # - Terminal Server (windows server 2012 R2 评估板 带桌面体验,注册表也是这个值)
    # - Terminal Server (windows server 2022 R2 评估板 不带桌面体验,注册表也是这个值)
    # - Terminal Server (WES7 / Thin PC)
    # - ?               (windows 10/11 enterprise 多会话)

    # 用内核版本号筛选驱动
    # 使得可以安装 Hyper-V Server / Azure Stack HCI 等 Windows Server 变种
    # 7601.24214.180801-1700.win7sp1_ldr_escrow_CLIENT_ULTIMATE_x64FRE_en-us.iso wim 没有 Installation Type
    # Vista wim 和 注册表 都没有 InstallationType
    if false; then
        nt_ver=$(get_selected_image_prop "Major Version").$(get_selected_image_prop "Minor Version")
        build_ver=$(get_selected_image_prop "Build")
        installation_type=$(get_selected_image_prop "Installation Type")
    fi

    # 挂载 install.wim,检查
    # 1. 是否自带 sac 组件
    # 2. 是否自带 nvme 驱动
    # 3. 是否支持 sha256
    # 4. Installation Type
    # shellcheck disable=SC2046
    wimmount "$iso_install_wim" "$image_index" /wim/ \
        $($is_swm && echo --ref=$(dirname "$iso_install_wim")/$swm_ref)

    # 获取版本号
    get_windows_version_from_windows_drive /wim

    # 检测 client/server,并转换成标准版 windows 名称
    windows_type=$(get_windows_type_from_windows_drive /wim)
    product_ver=$(
        case "$windows_type" in
        client) get_client_name_by_build_ver "$build_ver" ;;
        server) get_server_name_by_build_ver "$build_ver" ;;
        esac
    )

    # 检测 sac 和 nvme
    {
        find_file_ignore_case /wim/Windows/System32/sacsess.exe && has_sac=true || has_sac=false
        find_file_ignore_case /wim/Windows/INF/stornvme.inf && has_stornvme=true || has_stornvme=false
    } >/dev/null 2>&1

    # 检测是否支持 sha256 签名的驱动
    support_sha256=false
    if is_nt_ver_ge 6.2; then
        support_sha256=true
    else
        # 安装环境下 drvload.exe 不会验证签名,能安装 sha256 的驱动
        # 但重启后提示 Windows cannot verify the digital signature for this file.

        # winload.exe/efi 有这串字符
        # Windows cannot verify the digital signature for this file.
        # strings -e l winload.exe | grep -i signature
        # strings -e l winload.efi | grep -i signature

        # 硬盘控制器驱动是 boot-start 驱动,由 winload.exe/efi 验证签名
        # 网卡驱动不是 boot-start 驱动,由 ci.dll 验证签名

        # win7 sp1 iso 不支持 sha256 的驱动,但是
        # ci.dll      能找到 8+64 个常量和 oid 0609608648016503040201 0102040365014886600906
        # winload.exe 能找到 8+64 个常量和 oid 0609608648016503040201 0102040365014886600906
        # winload.efi 能找到 8+64 个常量和 oid     608648016503040201

        # 官网有提到 KB3033929 和 KB4039648, 应该分别是 2008r2 和 2008 最早支持 sha256 的补丁
        # https://support.microsoft.com/kb/4472027#:~:text=KB3033929%20%E5%92%8C%20KB4039648
        # https://support.drweb.cn/sha2
        # https://support.kaspersky.com/common/compatibility/15761
        # https://www.internetdownloadmanager.com/register/new_faq/sha256-support-for-outdated-versions-of-Windows.html
        # https://www.catalog.update.microsoft.com/

        # vista sp2 iso
        # 用 KB4039648 和 KB4090450 做测试,独立安装时,注册表没有发现另一个 KB 的痕迹
        # 后续很多补丁如果包含 winload.exe/efi,都是支持 sha256 的新版,因此不能通过检测 KB 编号来判断
        # HKEY_LOCAL_MACHINE\Microsoft\Windows\CurrentVersion\Component Based Servicing\Package
        # HKEY_LOCAL_MACHINE\Microsoft\Windows\CurrentVersion\Component Based Servicing\PackageDetect

        # vista sp2 iso 独立安装以下补丁时
        # 补丁          发布日期   BuildLabEx          ubr    winload.exe   winload.efi   ci.dll
        # KB4039648 旧  2018/2/21  6002.18005(没改变)  没有   6002.24259    6002.24283    6002.24259
        # KB4039648 新  2018/3/22  6002.18005(没改变)  没有   6002.24259    6002.24298    6002.24259
        # KB4039648-v2  2018/6/12  6002.24381          没有   6002.24362    6002.24381    6002.24259
        # KB4474419-v4  2019/10/8  6003.20555          没有   6003.20505    6003.20555    6003.20593

        # win7 sp1 iso 独立安装以下补丁时
        # KB3033929     2015/3/10  7601.18741          没有   18649/22854  18741/22948    18519/22730
        # KB4474419-v3  2019/9/10  7601.24384          没有         24149        24384          24158

        # 最早的 KB4039648 KB3033929 都支持 sha256
        # winload.exe/efi 版本号 >= ci.dll
        # 因此用 winload.exe/efi 的版本号来判断是否支持 sha256

        apk add pev
        local maj min build rev
        winload=$(find_file_ignore_case "/wim/Windows/System32/winload.$(is_efi && echo efi || echo exe)")
        IFS=. read -r maj min build rev \
            < <(peres -v "$winload" | grep 'Product Version:' | awk '{print $NF}')
        apk del pev

        # vista/2008
        # https://support.microsoft.com/kb/KB4039648
        # https://catalog.update.microsoft.com/Search.aspx?q=KB4039648

        # win7/2008r2 网页有列出文件版本号
        # https://support.microsoft.com/kb/KB3033929
        # https://catalog.update.microsoft.com/Search.aspx?q=KB3033929

        # rev 1xxxx 是 GDR 分支
        # rev 2xxxx 是 LDR 分支

        # vista/2008 版本从 6002 到 6003, rev 减少 4000
        # https://support.microsoft.com/topic/1335e4d4-c155-52eb-4a45-b85bd1909ca8

        if is_efi; then
            if { [ "$maj.$min" = 6.1 ] && [ "$build" -eq 7601 ] && [ "$rev" -ge 22948 ]; } ||
                { [ "$maj.$min" = 6.1 ] && [ "$build" -eq 7601 ] && [ "$rev" -ge 18741 ] && [ "$rev" -lt 20000 ]; } ||
                { [ "$maj.$min" = 6.0 ] && [ "$build" -eq 6003 ] && [ "$rev" -ge 20283 ]; } ||
                { [ "$maj.$min" = 6.0 ] && [ "$build" -eq 6002 ] && [ "$rev" -ge 24283 ]; }; then
                support_sha256=true
            fi
        else
            if { [ "$maj.$min" = 6.1 ] && [ "$build" -eq 7601 ] && [ "$rev" -ge 22854 ]; } ||
                { [ "$maj.$min" = 6.1 ] && [ "$build" -eq 7601 ] && [ "$rev" -ge 18649 ] && [ "$rev" -lt 20000 ]; } ||
                { [ "$maj.$min" = 6.0 ] && [ "$build" -eq 6003 ] && [ "$rev" -ge 20259 ]; } ||
                { [ "$maj.$min" = 6.0 ] && [ "$build" -eq 6002 ] && [ "$rev" -ge 24259 ]; }; then
                support_sha256=true
            fi
        fi
    fi

    wimunmount /wim/

    info "Selected image info"
    echo "Image Name: $image_name"
    echo "Product Version: $product_ver"
    echo "Windows Type: $windows_type"
    echo "NT Version: $nt_ver"
    echo "Build Version: $build_ver"
    echo "Revision Version: $rev_ver"
    echo "-------------------------"
    echo "Has SAC: $has_sac"
    echo "Has StorNVMe: $has_stornvme"
    echo "Support SHA256: $support_sha256"
    echo "-------------------------"
    echo

    # 复制 boot.wim 到 /os,用于临时编辑
    if [ -n "$boot_wim" ]; then
        # 自定义 boot.wim 链接
        download "$boot_wim" /os/boot.wim
    else
        cp /iso/$sources_boot_wim /os/boot.wim
    fi

    # efi 启动目录为 efi 分区
    # bios 启动目录为 os 分区
    if is_efi; then
        boot_dir=/os/boot/efi
    else
        boot_dir=/os
    fi

    # 复制 iso 根目录 boot 开头的文件
    echo 'Copying boot files...'
    find /iso -maxdepth 1 -iname 'boot*' -exec cp -r {} "$boot_dir" \;

    # efi 额外复制 iso 根目录 efi 文件夹
    if is_efi; then
        echo 'Copying efi files...'
        find /iso -maxdepth 1 -type d -iname efi -exec cp -r {} "$boot_dir" \;
    fi

    # 复制iso全部文件(除了boot.wim)到installer分区
    echo 'Copying installer files...'
    if false; then
        # 还需忽略大小写
        rsync -rv \
            --exclude=/sources/boot.wim \
            --exclude=/sources/install.wim \
            --exclude=/sources/install.esd \
            --exclude='/sources/install*.swm' \
            /iso/* /os/installer/
    else
        (
            cd /iso
            find . -type f \
                -not -iname boot.wim \
                -not -iname install.wim \
                -not -iname install.esd \
                -not -iname 'install*.swm' \
                -exec cp -r --parents {} /os/installer/ \;
        )
    fi

    # 如果是 swm,要先合并成 wim 才能编辑
    if $is_swm; then
        install_wim=$(echo "$install_wim" | sed 's/\.swm$/.wim/i')
        # 防止不格盘二次运行时报错:文件已存在
        rm -f "$install_wim"
        wimexport --ref="$(dirname "$iso_install_wim")/$swm_ref" "$iso_install_wim" "$image_index" "$install_wim"
        # 只导出了要安装的镜像,因此 image_index 变为 1
        image_index=1
    elif false; then
        # 优化 install.wim
        # 优点: 可以节省 200M~600M 空间,用来创建虚拟内存
        #       (意义不大,因为已经删除了 boot.wim 用来创建虚拟内存,vista 除外)
        # 缺点: 如果 install.wim 只有一个镜像,则只能缩小 10M+
        time wimexport --threads "$(get_build_threads 512)" "$iso_install_wim" "$image_index" "$install_wim"
        # 只导出了要安装的镜像,因此 image_index 变为 1
        image_index=1
        info "install.wim size"
        echo "Original:  $(get_filesize_mb "$iso_install_wim")"
        echo "Optimized: $(get_filesize_mb "$install_wim")"
        echo
    else
        cp "$iso_install_wim" "$install_wim"
    fi

    # win11 要求 1GHz 2核(1核超线程也行)
    # 用注册表无法绕过
    # https://github.com/pbatard/rufus/issues/1990
    # https://learn.microsoft.com/windows/iot/iot-enterprise/Hardware/System_Requirements
    # win11 旧版本安装程序(24h2之前)无法用 setup.exe /product server 跳过 cpu 核数限制,因此在xml里解除限制
    if [ "$product_ver" = "11" ] && [ "$(nproc)" -le 1 ]; then
        wiminfo "$install_wim" "$image_index" --image-property WINDOWS/INSTALLATIONTYPE=Server
    fi

    # 变量名     使用场景
    # arch_uname arch命令 / uname -m                      x86_64   aarch64
    # arch_wim   wiminfo                             x86  x86_64   ARM64
    # arch       virtio iso / unattend.xml / .inf    x86  amd64    arm64
    # arch_xdd   virtio msi / xen驱动                x86   x64
    # arch_dd    华为云驱动                           32    64

    # 将 wim 的 arch 转为驱动和应答文件的 arch
    case "$arch_wim" in
    x86)
        arch=x86
        arch_xdd=x86
        arch_dd=32
        ;;
    x86_64)
        arch=amd64
        arch_xdd=x64
        arch_dd=64
        ;;
    arm64)
        arch=arm64
        arch_xdd= # xen 没有 arm64 驱动,# virtio 也没有 arm64 msi
        arch_dd=  # 华为云没有 arm64 驱动
        ;;
    esac

    # win7 drvload 可以加载 sha256 签名的驱动
    # 但系统安装完重启报错 windows cannot verify the digital signature for this file
    # 需要按 F8 禁用驱动签名

    add_drivers() {
        info "Add drivers"

        drv=/os/drivers
        mkdir -p "$drv" # 驱动下载临时文件夹

        # 这里有坑
        # $(get_cloud_vendor) 调用了 cache_dmi_and_virt
        # 但是 $(get_cloud_vendor) 运行在 subshell 里面
        # subshell 运行结束后里面的变量就消失了
        # 因此先运行 cache_dmi_and_virt
        cache_dmi_and_virt
        vendor="$(get_cloud_vendor)"

        # virtio
        if is_virt_contains virtio; then
            if [ "$vendor" = aliyun ] && is_nt_ver_ge 6.1 && [ "$arch_wim" = x86_64 ]; then
                add_driver_aliyun_virtio
            elif [ "$vendor" = qcloud ] && is_nt_ver_ge 6.1 && [ "$arch_wim" = x86_64 ]; then
                add_driver_qcloud_virtio
            # 未测试是否需要专用驱动
            elif false && [ "$vendor" = huawei ] && is_nt_ver_ge 6.0 && { [ "$arch_wim" = x86 ] || [ "$arch_wim" = x86_64 ]; }; then
                add_driver_huawei_virtio

            # gcp 官方驱动不全,需要用公版补全
            # 官方 windows server 模板没有 viorng 设备,但 linux 模板有
            elif [ "$vendor" = gcp ] && is_nt_ver_ge 6.1 && [ "$arch_wim" = x86 ] && $support_sha256; then
                add_driver_gcp_virtio
                add_driver_generic_virtio \( -iname viorng.inf -or -iname pvpanic.inf \)

            elif [ "$vendor" = gcp ] && is_nt_ver_ge 6.1 && [ "$arch_wim" = x86_64 ] && $support_sha256; then
                add_driver_gcp_virtio
                add_driver_generic_virtio -iname viorng.inf

            elif [ "$vendor" = gcp ] && [ "$nt_ver" = 6.1 ] && [ "$arch_wim" = x86_64 ] && ! $support_sha256; then
                add_driver_gcp_virtio_win6_1_sha1_x64
                add_driver_generic_virtio \( -iname viorng.inf -or -iname balloon.inf \)

            else
                # 兜底
                add_driver_generic_virtio
            fi
        fi

        # xen
        if is_virt_contains xen; then
            # generic_xen 兜底,但未签名,暂停使用
            if is_nt_ver_ge 6.1 && [ "$arch_wim" = x86_64 ]; then
                add_driver_aws_xen
            elif is_nt_ver_ge 6.0 && { [ "$arch_wim" = x86 ] || [ "$arch_wim" = x86_64 ]; }; then
                add_driver_citrix_xen
            fi
        fi

        # vmd
        # RST v17 不支持 vmd
        # RST v18 inf 要求 15063 或以上
        # RST v19 inf 要求 15063 或以上
        # RST v20 inf 要求 19041 或以上
        if [ -d /sys/module/vmd ] && [ "$build_ver" -ge 15063 ] && [ "$arch_wim" = x86_64 ]; then
            add_driver_vmd
        fi

        # 厂商驱动
        case "$vendor" in
        aws)
            if is_nt_ver_ge 6.1 && { [ "$arch_wim" = x86_64 ] || [ "$arch_wim" = arm64 ]; }; then
                add_driver_aws
            fi
            ;;
        azure)
            # inf 不限版本,未测试
            if [ "$arch_wim" = x86 ] || [ "$arch_wim" = x86_64 ]; then
                add_driver_azure
            fi
            ;;
        gcp)
            # inf 不限版本,6.0 能装但用不了
            # x86 x86_64 arm64 都有
            add_driver_gcp
            ;;
        esac

        # intel 网卡驱动
        # 官网没有提供 vista/2008 驱动
        # win7 驱动 inf/ndis 不支持 vista/2008
        if is_nt_ver_ge 6.1 && { [ "$arch_wim" = x86 ] || [ "$arch_wim" = x86_64 ]; } &&
            grep -iq 8086 /sys/class/net/e*/device/vendor; then
            add_driver_intel_nic
        fi

        # 自定义驱动
        add_driver_custom
    }

    add_driver_intel_nic() {
        info "Add drivers: Intel NIC"

        arch_intel=$(
            case "$arch_wim" in
            x86) echo 32 ;;
            x86_64) echo x64 ;;
            esac
        )

        url=$(
            case "$product_ver" in
            '7' | '2008 r2')
                # 现在官网只有 25.0
                # 25.0 比 24.5 只更新了 ProSet 软件,驱动相同
                # 25.0 有部分文件是 sha256 签名
                # 24.3 全部文件是 sha1 签名
                # https://web.archive.org/web/20250405130938/https://www.intel.com/content/www/us/en/download/15590/29323/intel-network-adapter-driver-for-windows-7-final-release.html
                echo https://downloadmirror.intel.com/18713/eng/prowin${arch_intel}legacy.exe
                ;;
            '8' | '8.1')
                # 之前有 Intel® Network Adapter Driver for Windows 8* - Final Release ,版本 22.7.1
                # 但已被删除,原因不明
                # https://web.archive.org/web/20250501043104/https://www.intel.com/content/www/us/en/download/16765/intel-network-adapter-driver-for-windows-8-final-release.html
                # 27.8 有 NDIS63 文件夹,意味着支持 Windows 8
                # 27.8 相比 22.7.1,可能有些老设备不支持了,但我们不管了
                echo https://downloadmirror.intel.com/764813/Wired_driver_27.8_${arch_intel}.zip
                ;;
            '2012' | '2012 r2')
                echo https://downloadmirror.intel.com/772074/Wired_driver_28.0_${arch_intel}.zip
                ;;
            # 2016 2019 2022 2025 win10 win11
            *) case "${arch_intel}" in
                32)
                    echo https://downloadmirror.intel.com/849483/Wired_driver_30.0.1_${arch_intel}.zip
                    ;;
                x64)
                    id=$(
                        case "$product_ver" in
                        10) echo 18293 ;;
                        11) echo 727998 ;;
                        2016) echo 18737 ;;
                        2019) echo 19372 ;;
                        2022) echo 706171 ;;
                        2025) echo 838943 ;;
                        esac
                    )
                    # intel 禁止了 wget 下载网页
                    wget -U curl/7.54.1 https://www.intel.com/content/www/us/en/download/$id.html -O- |
                        grep -Eio -m1 "\"https://.+/(Wired_driver|prowin).*${arch_intel}(legacy)?\.(zip|exe)\"" | tr -d '"' | grep .
                    ;;
                esac ;;
            esac
        )

        # 注意 intel 禁止了 aria2 下载
        download "$url" $drv/intel.zip

        # inf 可能是 UTF-16 LE?因此用 rg 搜索
        # 用 busybox unzip 解压 win10 驱动时,路径和文件名会粘在一起
        # 但解压 28.0 驱动时,依然会出现这个问题
        # 因此需要 convert_backslashes
        apk add unzip ripgrep

        # https://superuser.com/questions/1382839/zip-files-expand-with-backslashes-on-linux-no-subdirectories
        convert_backslashes() {
            for file in "$1"/*\\*; do
                if [ -f "$file" ]; then
                    target="${file//\\//}"
                    mkdir -p "${target%/*}"
                    mv -v "$file" "$target"
                fi
            done
        }

        # win7 驱动是 .exe 解压不会报错
        # win10 驱动是 .zip 解压反而会报错,目测 zip 文件有问题
        # 在 windows 下解压 win8 的驱动会提示 checksum 错误
        unzip -o -d $drv/intel/ $drv/intel.zip || true
        convert_backslashes $drv/intel

        is_have_inf_in_intel_dir() {
            find $drv/intel -ipath "*/*.inf" | grep . >/dev/null
        }

        # Wired_driver_28.0_x64.zip 需要二次解压
        if ! is_have_inf_in_intel_dir; then
            unzip -o -d $drv/intel/ $drv/intel/Wired_driver_*.exe || true
            convert_backslashes $drv/intel
        fi

        # 由于上面使用了 || true,因此确认下解压后是否有 inf 文件
        if ! is_have_inf_in_intel_dir; then
            error_and_exit "No .inf file found in intel driver package"
        fi

        # Vista RTM 版本号是 6000    NDIS 6.0
        # 2008  RTM 版本号是 6001    NDIS 6.1

        # 找出驱动文件夹对应的最低系统版本
        # 1. 驱动可能限制 windows client/server,但我们不区分
        #    如果装不了也没关系。如果能装但不加载,用户也可以在硬件管理器强制加载驱动
        # 2. 官网写着 win10 驱动要求 RS5 1809,但是驱动包里有 NDIS65 文件夹,也就是支持 10240
        # 3. 有可能 NDIS65 文件夹实际要求 NDIS 6.51?但是先不管
        # https://learn.microsoft.com/en-us/windows-hardware/drivers/network/overview-of-ndis-versions
        min_support_map=$(cat <>> 2k8
        # 10 >>> w10
        # 2012 r2 >>> 2k12R2
        virtio_sys=$(
            case "$(echo "$product_ver" | to_lower)" in
            'vista') echo 2k8 ;; # 没有 vista 文件夹
            *)
                case "$windows_type" in
                client) echo "w$product_ver" ;;
                server) echo "$product_ver" | sed -E -e 's/ //' -e 's/^200?/2k/' -e 's/r2/R2/' ;;
                esac
                ;;
            esac
        )

        # win7-drivers 分支 win7 文件夹只有一次提交,也就是 173 全家桶
        # 1. 2020.1.24 https://github.com/virtio-win/virtio-win-pkg-scripts/tree/win7-drivers/data/old-drivers/Win7

        # master 分支 win7 文件夹有 3 次提交,从古到今
        # https://github.com/virtio-win/virtio-win-pkg-scripts/commits/master/data/old-drivers/Win7
        # 1. 2020/6/4  sha256,176 全家桶,相当于没发布的 176 iso
        # 2. 2020/8/10 将部分文件降到 17400,相当于 189~215 iso
        # 3. 2022/4/14 将部分文件降级,相当于 217~最新版 iso

        # 可改成直接从 github commit 下载 win7 173(sha1) 176(sha256) 全家桶?
        # 国内可使用 jsdelivr 加速 github

        # 2k12
        # https://github.com/virtio-win/virtio-win-pkg-scripts/issues/61
        # 217 ~ 271    2k12 证书有问题,红帽的 virtio-win-1.9.45 没问题

        # win7
        # https://fedorapeople.org/groups/virt/virtio-win/repo/stable/
        # https://github.com/virtio-win/virtio-win-pkg-scripts/issues/40
        # 171-1     sha1   稳定版
        # 173-9     sha1   对应上面的 win7-drivers 分支,最后一次编译 win7 + sha1,但不是稳定版?
        # 176       sha256 对应上面的 master-1  最后一次编译 win7,从这次开始是 sha256,此次不提供 iso,编译的文件在之后的 iso 可以找到
        # 185 ~ 187 sha256 正常工作,win7 文件来自 176
        # 189 ~ 215 sha1   对应上面的 master-2  气球版本 17400,vultr 死机
        # 217 ~ 271 sha1   对应上面的 master-3  甲骨文 vioscsi 因硬件 ID 不同用不了,红帽的 virtio-win-1.9.45 也是

        # 甲骨文 vioscsi 硬件 ID 是 PCI\VEN_1AF4&DEV_1004&SUBSYS_0008108E&REV_00
        # SUBSYS 的厂商 ID 是甲骨文

        # virtio-win-0.1.173-9
        # %VirtioScsi.DeviceDesc% = scsi_inst, PCI\VEN_1AF4&DEV_1004&SUBSYS_00081AF4&REV_00, PCI\VEN_1AF4&DEV_1004
        # %VirtioScsi.DeviceDesc% = scsi_inst, PCI\VEN_1AF4&DEV_1048&SUBSYS_11001AF4&REV_01, PCI\VEN_1AF4&DEV_1048

        # stable-virtio
        # %RHELScsi.DeviceDesc% = rhelscsi_inst, PCI\VEN_1AF4&DEV_1004&SUBSYS_00081AF4&REV_00
        # %RHELScsi.DeviceDesc% = rhelscsi_inst, PCI\VEN_1AF4&DEV_1048&SUBSYS_11001AF4&REV_01

        case "$nt_ver" in
        6.0 | 6.1) $support_sha256 &&
            dir=archive-virtio/virtio-win-0.1.187-1 ||
            dir=archive-virtio/virtio-win-0.1.173-9 ;;        # vista|w7|2k8|2k8R2
        6.2 | 6.3) dir=archive-virtio/virtio-win-0.1.215-2 ;; # w8|w8.1|2k12|2k12R2
        *) dir=stable-virtio ;;
        esac

        # vista|w7|2k8|2k8R2|arm64 要从 iso 获取驱动
        if [ "$nt_ver" = 6.0 ] || [ "$nt_ver" = 6.1 ] || [ "$arch_wim" = arm64 ]; then
            virtio_source=iso
        else
            virtio_source=msi
        fi

        baseurl=https://fedorapeople.org/groups/virt/virtio-win/direct-downloads

        if [ "$virtio_source" = iso ]; then
            download $baseurl/$dir/virtio-win.iso $drv/virtio.iso
            mkdir -p $drv/virtio
            mount -o ro $drv/virtio.iso $drv/virtio

            # vista 如果安装气动驱动,会报错 windows could not configure one or more system components
            # 2008 安装的气球驱动不能用,需要到硬件管理器重新安装设备才能用,无需更新驱动
            if [ "$product_ver" = vista ]; then
                cp_drivers $drv/virtio -ipath "*/$virtio_sys/$arch/*" "$@" -not -ipath "*/balloon/*"
            else
                cp_drivers $drv/virtio -ipath "*/$virtio_sys/$arch/*" "$@"
            fi
        else
            # coreutils 的 cp mv rm 才有 -v 参数
            apk add 7zip file coreutils
            download $baseurl/$dir/virtio-win-gt-$arch_xdd.msi $drv/virtio.msi
            match="FILE_*_${virtio_sys}_${arch}*"
            7z x $drv/virtio.msi -o$drv/virtio -i!$match -y -bb1

            # 为没有后缀名的文件添加后缀名
            (
                cd $drv/virtio
                echo "Recognizing file extension..."
                for file in *"${virtio_sys}_${arch}"; do
                    recognized=false
                    maybe_exts=$(file -b --extension "$file")

                    # exe/sys -> sys
                    # exe/com -> exe
                    # dll/cpl/tlb/ocx/acm/ax/ime -> dll
                    for ext in sys exe dll; do
                        if echo $maybe_exts | grep -qw $ext; then
                            recognized=true
                            mv -v "$file" "$file.$ext"
                            break
                        fi
                    done

                    # 如果识别不了后缀名,就删除此文件
                    # 因为用不了,免得占用空间
                    if ! $recognized; then
                        rm -fv "$file"
                    fi
                done

                # 将
                # FILE_netkvm_netkvmco_w8.1_amd64.dll
                # FILE_netkvm_w8.1_amd64.cat
                # 改名为
                # netkvmco.dll
                # netkvm.cat
                echo "Renaming files..."
                for file in *; do
                    new_file=$(echo "$file" | sed "s|FILE_||; s|_${virtio_sys}_${arch}||; s|.*_||")
                    mv -v "$file" "$new_file"
                done
            )
            cp_drivers $drv/virtio "$@"
        fi
    }

    add_driver_qcloud_virtio() {
        info "Add drivers: QCloud virtio"

        # 测试版?
        # https://mirrors.tencent.com/install/cts/windows/Drivers.zip

        apk add 7zip
        download https://mirrors.tencent.com/install/windows/virtio_64_1.0.9.exe $drv/virtio.exe
        exclude='$*' # 排除 $PLUGINSDIR
        override=u   # A(u)to rename all
        7z x $drv/virtio.exe -o$drv/qcloud/ -ao$override -x!$exclude

        # balloon     6.2
        # balloon_1   6.1

        # netkvm      10.0
        # netkvm_1    6.1
        # netkvm_2    6.3

        # viostor     10.0
        # viostor_1   6.1
        # viostor_2   6.2

        drivers=$(
            case "$nt_ver" in
            6.1) echo balloon_1 netkvm_1 viostor_1 ;; # sha1
            6.2) echo balloon netkvm_1 viostor_2 ;;
            6.3) echo balloon netkvm_2 viostor_2 ;;
            *) echo balloon netkvm viostor ;;
            esac
        )

        for old_name in $drivers; do
            part=${old_name%%_*}
            if ! [ "$old_name" = "$part" ]; then
                find $drv/qcloud/$part -type f -iname "$old_name.*" | while read -r file; do
                    ext="${file##*.}"
                    mv -v "$file" "$drv/qcloud/$part/$part.$ext"
                done
            fi
            cp_drivers $drv/qcloud/$part/$part.inf
        done
    }

    add_driver_huawei_virtio() {
        info "Add drivers: Huawei virtio"

        huawei_sys=$(
            case "$(echo "$product_ver" | to_lower)" in
            vista) echo Vista2008 ;;
            7) echo 7 ;;
            8) [ "$arch_wim" = x86 ] && echo 7 || echo 2012 ;;      # 没有 win8 32/64
            8.1) [ "$arch_wim" = x86 ] && echo 7 || echo 2012_R2 ;; # 没有 win8.1 32/64
            10 | 11) echo 10 ;;
            2008) echo Vista2008 ;;
            '2008 r2') echo 2008_R2 ;;
            2012) [ "$arch_wim" = x86 ] && echo 2008_R2 || echo 2012 ;; # 没有 2012 32
            '2012 r2') echo 2012_R2 ;;
            2016 | 2019 | 202*) echo 2016 ;;
            esac
        )

        download https://ecs-instance-driver.obs.cn-north-1.myhuaweicloud.com/vmtools-windows.zip $drv/vmtools-windows.zip
        unzip -o -d $drv $drv/vmtools-windows.zip
        mkdir -p $drv/huawei
        mount -o ro $drv/vmtools-windows.iso $drv/huawei

        cp_drivers $drv/huawei -ipath "*/upgrade/windows ${huawei_sys}_${arch_dd}/drivers/*"
    }

    add_driver_aliyun_virtio() {
        info "Add drivers: Aliyun virtio"

        aliyun_sys=$(
            case "$nt_ver" in
            6.1) echo 2008R2 ;;
            6.2 | 6.3) echo 2012R2 ;; # 实际上是 2012 的驱动
            *) echo 2016 ;;
            esac
        )

        subdir=
        if [ "$nt_ver" = 6.1 ] && ! $support_sha256; then
            subdir=58017/ # sha1
        fi

        region=cn-hangzhou

        download https://windows-driver-$region.oss-$region.aliyuncs.com/virtio/${subdir}AliyunVirtio_WIN$aliyun_sys.zip \
            $drv/AliyunVirtio.zip
        unzip -o -d $drv $drv/AliyunVirtio.zip

        apk add innoextract
        innoextract -d $drv/aliyun/ $drv/AliyunVirtio_*_WIN${aliyun_sys}_$arch_xdd.exe
        apk del innoextract

        cp_drivers $drv/aliyun -ipath "*/C$/Program Files/AliyunVirtio/*/drivers/*"
    }

    # gcp virtio win7 x64 sha1
    # 缺 balloon viorng
    add_driver_gcp_virtio_win6_1_sha1_x64() {
        info "Add drivers: GCP virtio win6.1 sha1 x64"

        # 用到 nvme 时才下载 nvme 驱动
        # 因为 win7 可以通过更新获得 nvme 驱动
        # 而且谷歌推荐使用微软 nvme 驱动
        # (google-compute-engine-driver-nvme 2.0.0 更新内容是删除谷歌 nvme 驱动)
        mkdir -p $drv/gce/win6.1sha1
        for file in \
            WdfCoInstaller01009.dll WdfCoInstaller01011.dll \
            netkvm.inf netkvm.cat netkvm.sys netkvmco.dll \
            pvpanic.inf pvpanic.sys pvpanic.cat \
            vioscsi.inf vioscsi.sys vioscsi.cat \
            $([ -d /sys/module/nvme ] && ! $has_stornvme && echo nvme.inf nvme64.cat nvme.sys); do
            download https://storage.googleapis.com/gce-windows-drivers-public/win6.1sha1/$file $drv/gce/win6.1sha1/$file
        done
        cp_drivers $drv/gce/win6.1sha1
    }

    # gcp virtio win7+ sha256
    # x86 缺 viorng pvpanic
    # x64 缺 viorng
    # https://github.com/GoogleCloudPlatform/compute-image-tools/tree/master/daisy_workflows/image_build/windows
    # 官方是从 https://console.cloud.google.com/storage/browser/gce-windows-drivers-public 下载驱动,安装系统后再 googet 更新驱动
    # 我们一步到位从 googet 下载驱动
    add_driver_gcp_virtio() {
        info "Add drivers: GCP virtio"

        mkdir -p $drv/gce
        gce_repo=https://packages.cloud.google.com/yuck
        download $gce_repo/repos/google-compute-engine-stable/index $drv/gce/gce.json
        for part in balloon netkvm pvpanic vioscsi; do
            # gcp 提供的 pvpanic 没有 x86 驱动
            if [ "$part" = pvpanic ] && [ "$arch_wim" = x86 ]; then
                continue
            fi

            mkdir -p $drv/gce/$part
            link=$(grep -o "/pool/.*-google-compute-engine-driver-$part.*\.goo" $drv/gce/gce.json)
            wget $gce_repo$link -O- | tar xz -C $drv/gce/$part

            [ "$arch_wim" = x86 ] && suffix=-32 || suffix=
            cp_drivers $drv/gce/$part -ipath "*/win$nt_ver$suffix/*"
        done
    }

    # gcp
    # x86 x86_64 arm64 都有
    # win7 驱动是 sha256 签名
    add_driver_gcp() {
        info "Add drivers: GCP"

        # https://packages.cloud.google.com/yuck/repos/google-compute-engine-stable/index
        # https://packages.cloud.google.com/yuck/repos/google-compute-engine-driver-gvnic-gq-stable/index
        # 官方镜像的 gvnic 是从 gvnic-gq-stable 获取的,版本低一点,但更稳定?

        mkdir -p $drv/gce
        gce_repo=https://packages.cloud.google.com/yuck
        download $gce_repo/repos/google-compute-engine-stable/index $drv/gce/gce.json
        for part in gvnic gga; do
            # gvnic 没有 arm64
            if [ "$part" = gvnic ] && [ "$arch_wim" = arm64 ]; then
                continue
            fi

            mkdir -p $drv/gce/$part
            link=$(grep -o "/pool/.*-google-compute-engine-driver-$part.*\.goo" $drv/gce/gce.json)
            wget $gce_repo$link -O- | tar xz -C $drv/gce/$part

            # inf 不限版本
            # 但 win7 gvnic ndis 版本是 6.2,vista/2008 能装但用不了
            # https://github.com/GoogleCloudPlatform/compute-virtual-ethernet-windows/blob/cad1edf7a05465f4972a81f2c015952fd228b5e3/src/gvnic.vcxproj#L298
            if false; then
                for suffix in '' '-32'; do
                    if [ -d "$drv/gce/$part/win6.1$suffix" ]; then
                        cp -r "$drv/gce/$part/win6.1$suffix" "$drv/gce/$part/win6.0$suffix"
                    fi
                done
            fi

            case "$part" in
            gvnic)
                [ "$arch_wim" = x86 ] && suffix=-32 || suffix=
                cp_drivers $drv/gce/gvnic -ipath "*/win$nt_ver$suffix/*"
                ;;
            gga)
                cp_drivers $drv/gce/gga -ipath "*/win$nt_ver/*"
                ;;
            esac
        done
    }

    # azure
    # https://learn.microsoft.com/azure/virtual-network/accelerated-networking-mana-windows
    add_driver_azure() {
        info "Add drivers: Azure"

        download https://aka.ms/manawindowsdrivers $drv/azure.zip
        unzip $drv/azure.zip -d $drv/azure/
        cp_drivers $drv/azure
    }

    add_driver_vmd() {
        # RST v20 不支持 11代 PCI\VEN_8086&DEV_9A0B
        support_v19=false
        support_v20=false
        for d in /sys/bus/pci/devices/*; do
            vendor=$(cat "$d/vendor" 2>/dev/null)
            device=$(cat "$d/device" 2>/dev/null)
            if [ "$vendor" = "0x8086" ]; then
                case "$device" in
                "0x9a0b") support_v19=true && support_v20=false && break ;;
                "0x467f") support_v19=true && support_v20=true && break ;;
                "0xa77f") support_v19=true && support_v20=true && break ;;
                "0x7d0b") support_v19=false && support_v20=true && break ;;
                "0xad0b") support_v19=false && support_v20=true && break ;;
                esac
            fi
        done

        local page=
        if $support_v20 && [ "$build_ver" -ge 19041 ]; then
            page=https://www.intel.com/content/www/us/en/download/849936.html
        elif $support_v19 && [ "$build_ver" -ge 15063 ]; then
            page=https://www.intel.com/content/www/us/en/download/849933.html
        fi

        if [ -n "$page" ]; then
            # intel 禁止了 wget 下载网页
            local url
            url=$(wget -U curl/7.54.1 "$page" -O- |
                grep -Eio -m1 "\"https://.+/SetupRST\.exe\"" | tr -d '"' | grep .)

            # 注意 intel 禁止了 aria2 下载
            download $url $drv/SetupRST.exe
            apk add 7zip
            7z x $drv/SetupRST.exe -o$drv/SetupRST -i!.text
            7z x $drv/SetupRST/.text -o$drv/vmd
            apk del 7zip
            cp_drivers $drv/vmd
        else
            # 如果开启了 vmd 但硬盘不在 vmd 上,linux 会自动加载 vmd 模块?
            # 还要判断主硬盘是否在 vmd 上,如果不在,即使没有 vmd 驱动也可继续安装
            # 因此目前先不中止脚本
            : error_and_exit "can't find suitable vmd driver"
        fi
    }

    # 脚本自动检测驱动可能有问题
    # 假设是 win7 时代的网卡,官网没有 win10 驱动,系统也不自带
    # 但实际上 win10 可以用 win7 的驱动
    # 这种情况即使脚本自动下载 win10 的驱动包,也不会包含这个驱动
    # 应该下载 win7 的驱动
    # 因此只能交给用户自己添加驱动

    add_driver_custom() {
        if [ -d /custom_drivers/ ]; then
            cp_drivers custom /custom_drivers/
            # 复制后不删除,因为脚本可能再次运行
        fi
    }

    # 修改应答文件
    download $confhome/windows.xml /tmp/autounattend.xml
    locale=$(get_selected_image_prop 'Default Language')
    use_default_rdp_port=$(is_need_change_rdp_port && echo false || echo true)
    password_base64=$(get_password_windows_administrator_base64)
    # 7601.24214.180801-1700.win7sp1_ldr_escrow_CLIENT_ULTIMATE_x64FRE_en-us.iso Image Name 为空
    # 将 xml Image Name 的值设为空可以正常安装
    sed -i \
        -e "s|%arch%|$arch|" \
        -e "s|%image_name%|$image_name|" \
        -e "s|%locale%|$locale|" \
        -e "s|%administrator_password%|$password_base64|" \
        -e "s|%use_default_rdp_port%|$use_default_rdp_port|" \
        /tmp/autounattend.xml

    # 修改应答文件,分区配置
    if is_efi; then
        sed -i "s|%installto_partitionid%|3|" /tmp/autounattend.xml
    else
        sed -i "s|%installto_partitionid%|1|" /tmp/autounattend.xml
    fi

    # vista/2008 有这行安装会报错
    if [ "$nt_ver" = 6.0 ]; then
        sed -i "/EnableFirewall/d" /tmp/autounattend.xml
    fi

    # 2012 r2,删除 key 字段,报错 Windows cannot read the  setting from the unattend answer file,即使创建 ei.cfg
    # ltsc 2021,有 ei.cfg,填空白 key 正常
    # ltsc 2021 n,有 ei.cfg,填空白 key 报错 Windows Cannot find Microsoft software license terms
    # 评估版 iso ei.cfg 有 EVAL 字样,填空白 key 报错 Windows Cannot find Microsoft software license terms

    # key
    if [ "$product_ver" = vista ]; then
        # vista 无人值守安装需要密钥,密钥可与 edition 不一致
        # https://learn.microsoft.com/en-us/windows-server/get-started/kms-client-activation-keys
        # 从镜像获取默认密钥
        setup_cfg=$(get_path_in_correct_case /os/installer/sources/inf/setup.cfg)
        key=$(del_cr <"$setup_cfg" | grep -Eix 'Value=([A-Z0-9]{5}-){4}[A-Z0-9]{5}' | cut -d= -f2 | grep .)
        sed -i "s/%key%/$key/" /tmp/autounattend.xml
    else
        if [ -f "$(get_path_in_correct_case /os/installer/sources/ei.cfg)" ]; then
            # 镜像有 ei.cfg,删除 key 字段
            sed -i "/%key%/d" /tmp/autounattend.xml
        else
            # 镜像无 ei.cfg,填空白 key
            sed -i "s/%key%//" /tmp/autounattend.xml
        fi
    fi

    # 挂载 boot.wim
    info "mount boot.wim"
    wimmountrw /os/boot.wim "$boot_index" /wim/

    # 防止重复
    copyed_infs=
    cp_drivers() {
        if [ "$1" = custom ]; then
            shift
            dst=$(get_path_in_correct_case "/wim/custom_drivers")
        else
            dst=$(get_path_in_correct_case "/wim/drivers")
        fi

        src=$1
        shift

        # -not -iname "*.pdb" \
        # -not -iname "dpinst.exe" \

        # 这里需要在 while 里面变更 $copyed_infs,因此不能用 find | while
        while read -r inf; do
            if ! is_list_has "$copyed_infs" "$inf"; then
                parse_inf_and_cp_driever "$inf" "$dst" "$arch" false
                copyed_infs=$(list_add "$copyed_infs" "$inf")
            fi
        done < <(find $src -type f -iname "*.inf" "$@")
    }

    # 添加驱动
    add_drivers

    # win7 要添加 bootx64.efi 到 efi 目录
    if is_efi; then
        [ $arch = amd64 ] && boot_efi=bootx64.efi || boot_efi=bootaa64.efi

        local src dst
        dst=$(get_path_in_correct_case /os/boot/efi/EFI/boot/$boot_efi)
        if ! [ -f $dst ]; then
            mkdir -p "$(dirname $dst)"
            src=$(get_path_in_correct_case /wim/Windows/Boot/EFI/bootmgfw.efi)
            cp "$src" "$dst"
        fi
    fi

    # 复制应答文件
    # 移除注释,否则 windows-setup.bat 重新生成的 autounattend.xml 有问题
    wim_autounattend_xml=$(get_path_in_correct_case /wim/autounattend.xml)
    wim_windows_xml=$(get_path_in_correct_case /wim/windows.xml)
    wim_setup_exe=$(get_path_in_correct_case /wim/setup.exe)

    apk add xmlstarlet
    xmlstarlet ed -d '//comment()' /tmp/autounattend.xml >$wim_autounattend_xml
    unix2dos $wim_autounattend_xml
    info "autounattend.xml"
    # 查看最终文件,并屏蔽密码
    xmlstarlet ed -d '//*[name()="AdministratorPassword" or name()="Password"]' $wim_autounattend_xml | cat -n
    apk del xmlstarlet

    # 避免无参数运行 setup.exe 时自动安装
    mv $wim_autounattend_xml $wim_windows_xml

    # 复制安装脚本
    # https://slightlyovercomplicated.com/2016/11/07/windows-pe-startup-sequence-explained/
    # https://learn.microsoft.com/previous-versions/windows/it-pro/windows-vista/cc721977(v=ws.10)
    mv $wim_setup_exe $wim_setup_exe.disabled

    # 如果有重复的 Windows/System32 文件夹,会提示找不到 winload.exe 无法引导
    # win7 win10  boot.wim 是 Windows/System32,install.wim 是 Windows/System32
    # win2016     boot.wim 是 windows/system32,install.wim 是 Windows/System32
    # wimmount 无法挂载成忽略大小写

    startnet_cmd=$(get_path_in_correct_case /wim/Windows/System32/startnet.cmd)
    winpeshl_ini=$(get_path_in_correct_case /wim/Windows/System32/winpeshl.ini)

    download $confhome/windows-setup.bat $startnet_cmd
    # dism 手动释放镜像时用
    # sed -i "s|@image_name@|$image_name|" "$startnet.cmd"

    # shellcheck disable=SC2154
    if [ "$force_old_windows_setup" = 1 ]; then
        sed -i 's/ForceOldSetup=0/ForceOldSetup=1/i' $startnet_cmd
    fi

    # 有 SAC 组件时,启用 EMS
    if $has_sac; then
        sed -i 's/EnableEMS=0/EnableEMS=1/i' $startnet_cmd
    fi

    # 4kn EFI 分区最少要 260M
    # https://learn.microsoft.com/windows-hardware/manufacture/desktop/hard-drives-and-partitions
    if is_4kn; then
        sed -i 's/is4kn=0/is4kn=1/i' $startnet_cmd
    fi

    # Windows Thin PC 有 Windows\System32\winpeshl.ini
    # [LaunchApps]
    # %SYSTEMDRIVE%\windows\system32\drvload.exe, %SYSTEMDRIVE%\windows\inf\sdbus.inf
    # %SYSTEMDRIVE%\setup.exe
    if [ -f "$winpeshl_ini" ]; then
        info "mod winpeshl.ini"
        # https://learn.microsoft.com/previous-versions/windows/it-pro/windows-vista/cc721977(v=ws.10)
        # 两种方法都可以,第一种是原版命令
        sed -i 's|setup.exe|windows\\system32\\cmd.exe, "/k %SYSTEMROOT%\\system32\\startnet.cmd"|i' "$winpeshl_ini"
        # sed -i 's|setup.exe|windows\\system32\\startnet.cmd|i' "$winpeshl_ini"
        cat -n "$winpeshl_ini"
    fi

    # 提交修改 boot.wim
    info "Unmount boot.wim"
    wimunmount --commit /wim/

    # 原地优化可以用以下命令之一
    # wimdelete /os/boot.wim 1
    # wimoptimize /os/boot.wim

    # 优化 boot.wim 并复制到正确的位置
    if is_nt_ver_ge 6.1; then
        # win7 或以上删除 boot.wim 镜像 1 不会报错
        # 因为 win7 winre 镜像在 install.wim Windows\System32\Recovery\winRE.wim
        images=$boot_index
    else
        # vista 删除 boot.wim 镜像 1 会报错
        # Windows cannot access the required file Drive:\Sources\Boot.wim.
        # Make sure all files required for installation are available and restart the installation.
        # Error code: 0x80070491
        # vista install.wim 没有 Windows\System32\Recovery\winRE.wim
        images=all
    fi
    mkdir -p "$(get_path_in_correct_case "$(dirname $boot_dir/$sources_boot_wim)")"
    # 防止不格盘二次运行时报错:文件已存在
    rm -f $boot_dir/$sources_boot_wim
    wimexport --boot /os/boot.wim "$images" $boot_dir/$sources_boot_wim
    info "boot.wim size"
    echo "Original:      $(get_filesize_mb /iso/$sources_boot_wim)"
    echo "Added Drivers: $(get_filesize_mb /os/boot.wim)"
    echo "Optimized:     $(get_filesize_mb "$boot_dir/$sources_boot_wim")"
    echo

    # vista 安装时需要 boot.wim,原因见上面
    if [ "$nt_ver" = 6.0 ] &&
        ! [ -e /os/installer/$sources_boot_wim ]; then
        cp $boot_dir/$sources_boot_wim /os/installer/$sources_boot_wim
    fi

    # windows 7 没有 invoke-webrequest
    # installer分区盘符不一定是D盘
    # 所以复制 resize.bat 到 install.wim
    if true; then
        info "mount install.wim"
        wimmountrw $install_wim "$image_index" /wim/
        if false; then
            # 使用 autounattend.xml
            # win7 在此阶段找不到网卡
            download $confhome/windows-resize.bat /wim/windows-resize.bat
            for ethx in $(get_eths); do
                create_win_set_netconf_script /wim/windows-set-netconf-$ethx.bat
            done
        else
            modify_windows /wim
        fi

        info "Unmount install.wim"
        wimunmount --commit /wim/
    fi

    # 添加引导
    if is_efi; then
        # 现在 add_default_efi_to_nvram() 添加 bootx64.efi 到最前面
        # 因此这里重复了
        if false; then
            apk add efibootmgr
            efibootmgr -c -L "Windows Installer" -d /dev/$xda -p1 -l "\\EFI\\boot\\$boot_efi"
        fi
    else
        # 或者用 ms-sys
        apk add grub-bios
        # efi 下,强制安装 mbr 引导,需要添加 --target i386-pc
        grub-install --target i386-pc --boot-directory="$(get_path_in_correct_case /os/boot)" /dev/$xda
        cat <"$(get_path_in_correct_case /os/boot/grub/grub.cfg)"
            set timeout=5
            menuentry "reinstall" {
                search --no-floppy --label --set=root os
                ntldr /$(cd /os && get_path_in_correct_case bootmgr)
            }
EOF
    fi
}

# 添加 netboot.efi 备用
download_netboot_xyz_efi() {
    dir=$1
    info "download netboot.xyz.efi"

    file=$dir/netboot.xyz.efi
    if [ "$(uname -m)" = aarch64 ]; then
        download https://boot.netboot.xyz/ipxe/netboot.xyz-arm64.efi $file
    else
        download https://boot.netboot.xyz/ipxe/netboot.xyz.efi $file
    fi
}

refind_main_disk() {
    if true; then
        apk add sfdisk
        main_disk=$(sfdisk --disk-id /dev/$xda | sed 's/0x//')
    else
        apk add lsblk
        # main_disk=$(blkid --match-tag PTUUID -o value /dev/$xda)
        main_disk=$(lsblk --nodeps -rno PTUUID /dev/$xda)
    fi
}

sync_time() {
    if false; then
        # arm要手动从硬件同步时间,避免访问https出错
        # do 机器第二次运行会报错
        hwclock -s || true
    fi

    # ntp 时间差太多会无法同步?
    # http 时间可能不准确,毕竟不是专门的时间服务器
    #      也有可能没有 date header?
    method=http

    case "$method" in
    ntp)
        if is_in_china; then
            ntp_server=ntp.aliyun.com
        else
            ntp_server=pool.ntp.org
        fi
        # -d[d]   Verbose
        # -n      Run in foreground
        # -q      Quit after clock is set
        # -p      PEER
        ntpd -d -n -q -p "$ntp_server"
        ;;
    http)
        url="$(grep -m1 ^http /etc/apk/repositories)/$(uname -m)/APKINDEX.tar.gz"
        # 可能有多行,取第一行
        date_header=$(wget -S --no-check-certificate --spider "$url" 2>&1 | grep -m1 '^  Date:')
        # gnu date 不支持 -D
        busybox date -u -D "  Date: %a, %d %b %Y %H:%M:%S GMT" -s "$date_header"
        ;;
    esac

    # 重启时 alpine 会自动写入到硬件时钟,因此这里跳过
    # hwclock -w
}

is_ubuntu_lts() {
    IFS=. read -r major minor < <(echo "$releasever")
    [ $((major % 2)) = 0 ] && [ $minor = 04 ]
}

get_ubuntu_kernel_flavor() {
    # 20.04/22.04 kvm 内核 vnc 没显示
    # 24.04 kvm = virtual
    # linux-image-virtual = linux-image-6.x-generic
    # linux-image-generic = linux-image-6.x-generic + amd64-microcode + intel-microcode + linux-firmware + linux-modules-extra-generic

    # TODO: ISO virtual-hwe-24.04 不安装 linux-image-extra-virtual-hwe-24.04 不然会花屏

    # https://github.com/systemd/systemd/blob/main/src/basic/virt.c
    # https://github.com/canonical/cloud-init/blob/main/tools/ds-identify
    # http://git.annexia.org/?p=virt-what.git;a=blob;f=virt-what.in;hb=HEAD
    if [ "$releasever" = 16.04 ]; then
        if is_virt; then
            echo virtual-hwe-$releasever
        else
            echo generic-hwe-$releasever
        fi
    else
        # 这里有坑
        # $(get_cloud_vendor) 调用了 cache_dmi_and_virt
        # 但是 $(get_cloud_vendor) 运行在 subshell 里面
        # subshell 运行结束后里面的变量就消失了
        # 因此先运行 cache_dmi_and_virt
        cache_dmi_and_virt
        vendor="$(get_cloud_vendor)"
        case "$vendor" in
        aws | gcp | oracle | azure | ibm) echo $vendor ;;
        *)
            is_ubuntu_lts && suffix=-hwe-$releasever || suffix=
            if is_virt; then
                echo virtual$suffix
            else
                echo generic$suffix
            fi
            ;;
        esac
    fi
}

install_redhat_ubuntu() {
    info "Download iso installer"

    # 安装 grub2
    if is_efi; then
        # 注意低版本的grub无法启动f38 arm的内核
        # https://forums.fedoraforum.org/showthread.php?330104-aarch64-pxeboot-vmlinuz-file-format-changed-broke-PXE-installs
        apk add grub-efi efibootmgr
        grub-install --efi-directory=/os/boot/efi --boot-directory=/os/boot
    else
        apk add grub-bios
        grub-install --boot-directory=/os/boot /dev/$xda
    fi

    # 重新整理 extra,因为grub会处理掉引号,要重新添加引号
    extra_cmdline=''
    for var in $(grep -o '\bextra_[^ ]*' /proc/cmdline | xargs); do
        if [[ "$var" = "extra_main_disk=*" ]]; then
            # 重新记录主硬盘
            refind_main_disk
            extra_cmdline="$extra_cmdline extra_main_disk=$main_disk"
        else
            extra_cmdline="$extra_cmdline $(echo $var | sed -E "s/(extra_[^=]*)=(.*)/\1='\2'/")"
        fi
    done

    # 安装红帽系时,只有最后一个有安装界面显示
    # https://anaconda-installer.readthedocs.io/en/latest/boot-options.html#console
    console_cmdline=$(get_ttys console=)
    grub_cfg=/os/boot/grub/grub.cfg

    # 新版grub不区分linux/linuxefi
    # shellcheck disable=SC2154
    if [ "$distro" = "ubuntu" ]; then
        download $iso /os/installer/ubuntu.iso
        mkdir -p /iso
        mount -o ro /os/installer/ubuntu.iso /iso

        # 内核风味
        kernel=$(get_ubuntu_kernel_flavor)

        # 要安装的版本
        # https://canonical-subiquity.readthedocs-hosted.com/en/latest/reference/autoinstall-reference.html#id
        # 20.04 不能选择 minimal ,也没有 install-sources.yaml
        source_id=
        if [ -f /iso/casper/install-sources.yaml ]; then
            ids=$(grep id: /iso/casper/install-sources.yaml | awk '{print $2}')
            if [ "$(echo "$ids" | wc -l)" = 1 ]; then
                source_id=$ids
            else
                [ "$minimal" = 1 ] && v= || v=-v
                source_id=$(echo "$ids" | grep $v '\-minimal')

                if [ "$(echo "$source_id" | wc -l)" -gt 1 ]; then
                    error_and_exit "find multi source id."
                fi
            fi
        fi

        # 正常写法应该是 ds="nocloud-net;s=https://xxx/" 但是甲骨文云的ds更优先,自己的ds根本无访问记录
        # $seed 是 https://xxx/
        cat <$grub_cfg
        set timeout=5
        menuentry "reinstall" {
            # https://bugs.launchpad.net/ubuntu/+source/grub2/+bug/1851311
            # rmmod tpm
            insmod all_video
            search --no-floppy --label --set=root installer
            loopback loop /ubuntu.iso
            linux (loop)/casper/vmlinuz iso-scan/filename=/ubuntu.iso autoinstall noprompt noeject cloud-config-url=$ks $extra_cmdline extra_kernel=$kernel extra_source_id=$source_id --- $console_cmdline
            initrd (loop)/casper/initrd
        }
EOF
    else
        download $vmlinuz /os/vmlinuz
        download $initrd /os/initrd.img
        download $squashfs /os/installer/install.img

        cat <$grub_cfg
        set timeout=5
        menuentry "reinstall" {
            insmod all_video
            search --no-floppy --label --set=root os
            linux /vmlinuz inst.stage2=hd:LABEL=installer:/install.img inst.ks=$ks $extra_cmdline $console_cmdline
            initrd /initrd.img
        }
EOF
    fi

    cat "$grub_cfg"
}

trans() {
    info "start trans"

    mod_motd

    # 先检查 modloop 是否正常
    # 防止格式化硬盘后,缺少 ext4 模块导致 mount 失败
    # https://github.com/bin456789/reinstall/issues/136
    ensure_service_started modloop

    cat /proc/cmdline
    clear_previous
    add_community_repo

    # 需要在重新分区之前,找到主硬盘
    # 重新运行脚本时,可指定 xda
    # xda=sda ash trans.start
    if [ -z "$xda" ]; then
        find_xda
    fi

    if [ "$distro" != "alpine" ]; then
        setup_web_if_enough_ram
        # util-linux 包含 lsblk
        # util-linux 可自动探测 mount 格式
        apk add util-linux
    fi

    # dd qemu 切换成云镜像模式,暂时没用到
    # shellcheck disable=SC2154
    if [ "$distro" = "dd" ] && [ "$img_type" = "qemu" ]; then
        # 移到 reinstall.sh ?
        distro=any
        cloud_image=1
    fi

    if is_use_cloud_image; then
        case "$img_type" in
        qemu)
            create_part
            download_qcow
            case "$distro" in
            centos | almalinux | rocky | oracle | redhat | anolis | opencloudos | openeuler)
                # 这几个系统云镜像系统盘是8~9g xfs,而我们的目标是能在5g硬盘上运行,因此改成复制系统文件
                install_qcow_by_copy
                ;;
            ubuntu)
                # 24.04 云镜像有 boot 分区(在系统分区之前),因此不直接 dd 云镜像
                install_qcow_by_copy
                ;;
            *)
                # debian fedora opensuse arch gentoo any
                dd_qcow
                resize_after_install_cloud_image
                modify_os_on_disk linux
                ;;
            esac
            ;;
        raw)
            # 暂时没用到 raw 格式的云镜像
            dd_raw_with_extract
            resize_after_install_cloud_image
            modify_os_on_disk linux
            ;;
        esac
    elif [ "$distro" = "dd" ]; then
        case "$img_type" in
        raw)
            dd_raw_with_extract
            if false; then
                # linux 扩容后无法轻易缩小,例如 xfs
                # windows 扩容在 windows 下完成
                resize_after_install_cloud_image
            fi
            if [ -d /configs/cloud-data ]; then
                modify_os_on_disk nocloud
            else
                modify_os_on_disk windows
            fi
            ;;
        qemu) # dd qemu 不可能到这里,因为上面已处理
            ;;
        esac
    else
        # 安装模式
        case "$distro" in
        alpine)
            install_alpine
            ;;
        arch | gentoo | aosc)
            create_part
            install_arch_gentoo_aosc
            ;;
        nixos)
            create_part
            install_nixos
            ;;
        fnos)
            create_part
            install_fnos
            ;;
        *)
            create_part
            mount_part_for_iso_installer
            case "$distro" in
            centos | almalinux | rocky | fedora | ubuntu | redhat) install_redhat_ubuntu ;;
            windows) install_windows ;;
            esac
            ;;
        esac
    fi

    # 需要用到 lsblk efibootmgr ,只要 1M 左右容量
    # 因此 alpine 不单独处理
    if is_efi; then
        del_invalid_efi_entry
        add_default_efi_to_nvram
    fi

    info 'done'
    # 让 web 输出全部内容
    sleep 5
}

# 脚本入口
# debian initrd 会寻找 main
# 并调用本文件的 create_ifupdown_config 方法
: main

# 复制脚本
# 用于打印错误或者再次运行
# 路径相同则不用复制
# 重点:要在删除脚本之前复制
if ! [ "$(readlink -f "$0")" = /trans.sh ]; then
    cp -f "$0" /trans.sh
fi
trap 'trap_err $LINENO $?' ERR

# 删除本脚本,不然会被复制到新系统
rm -f /etc/local.d/trans.start
rm -f /etc/runlevels/default/local

# 提取变量
extract_env_from_cmdline

# 带参数运行部分
# 重新下载并 exec 运行新脚本
if [ "$1" = "update" ]; then
    info 'update script'
    # shellcheck disable=SC2154
    wget -O /trans.sh "$confhome/trans.sh"
    chmod +x /trans.sh
    exec /trans.sh
elif [ "$1" = "alpine" ]; then
    info 'switch to alpine'
    distro=alpine
    # 后面的步骤很多都会用到这个,例如分区布局
    cloud_image=0
elif [ -n "$1" ]; then
    error_and_exit "unknown option $1"
fi

# 无参数运行部分
# 允许 ramdisk 使用所有内存,默认是 50%
mount / -o remount,size=100%

# 同步时间
# 1. 可以防止访问 https 出错
# 2. 可以防止 https://github.com/bin456789/reinstall/issues/223
#    E: Release file for http://security.ubuntu.com/ubuntu/dists/noble-security/InRelease is not valid yet (invalid for another 5h 37min 18s).
#    Updates for this repository will not be applied.
# 3. 不能直接读取 rtc,因为默认情况 windows rtc 是本地时间,linux rtc 是 utc 时间
# 4. 允许同步失败,因为不是关键步骤
sync_time || true

# 安装 ssh 并更改端口
apk add openssh
if is_need_change_ssh_port; then
    change_ssh_port / $ssh_port
fi

# 设置密码,添加开机启动 + 开启 ssh 服务
if is_need_set_ssh_keys; then
    set_ssh_keys_and_del_password /
    printf '\n' | setup-sshd
else
    change_root_password /
    printf '\nyes' | setup-sshd
fi

# 设置 frpc
# 并防止重复运行
if ls /configs/frpc.* >/dev/null 2>&1 && ! pidof frpc >/dev/null; then
    info 'run frpc'
    add_community_repo
    apk add frp
    while true; do
        frpc -c /configs/frpc.* || true
        sleep 5
    done &
fi

# shellcheck disable=SC2154
if [ "$hold" = 1 ]; then
    if is_run_from_locald; then
        info "hold"
        exit
    fi
fi

# 正式运行重装
# shellcheck disable=SC2046,SC2194
case 1 in
1)
    # ChatGPT 说这种性能最高
    exec > >(exec tee $(get_ttys /dev/) /reinstall.log) 2>&1
    trans
    ;;
2)
    exec > >(tee $(get_ttys /dev/) /reinstall.log) 2>&1
    trans
    ;;
3)
    trans 2>&1 | tee $(get_ttys /dev/) /reinstall.log
    ;;
esac

if [ "$hold" = 2 ]; then
    info "hold 2"
    exit
fi

# swapoff -a
# umount ?
sync
reboot


================================================
FILE: ttys.sh
================================================
#!/bin/sh
prefix=$1

# 不要在 windows 上使用,因为不准确
# 在原系统上使用,也可能不准确?例如安装了 cloud 内核的甲骨文?

# 注意 debian initrd 没有 xargs

# 最后一个 tty 是主 tty,显示的信息最全
is_first=true
if [ "$(uname -m)" = "aarch64" ]; then
    ttys="ttyS0 ttyAMA0 tty0"
else
    ttys="ttyS0 tty0"
fi

for tty in $ttys; do
    # hytron 有ttyS0 但无法写入
    if stty -g -F "/dev/$tty" >/dev/null 2>&1; then
        if $is_first; then
            is_first=false
        else
            printf " "
        fi

        printf "%s" "$prefix$tty"

        if [ "$prefix" = "console=" ] &&
            { [ "$tty" = ttyS0 ] || [ "$tty" = ttyAMA0 ]; }; then
            printf ",115200n8"
        fi
    fi
done


================================================
FILE: ubuntu-storage-early.sh
================================================
#!/bin/bash
sed -i -E '/^\.{3}$/d' /autoinstall.yaml
echo 'storage:' >>/autoinstall.yaml

# 禁用 swap
cat <>/autoinstall.yaml
  swap:
    size: 0
EOF

# 是用 size 寻找分区,number 没什么用
# https://curtin.readthedocs.io/en/latest/topics/storage.html
size_os=$(lsblk -bn -o SIZE /dev/disk/by-label/os)

# shellcheck disable=SC2154
if parted "/dev/$xda" print | grep '^Partition Table' | grep gpt; then
    # efi
    if [ -e /dev/disk/by-label/efi ]; then
        size_efi=$(lsblk -bn -o SIZE /dev/disk/by-label/efi)
        cat <>/autoinstall.yaml
  config:
    # disk
    - ptable: gpt
      path: /dev/$xda
      preserve: true
      type: disk
      id: disk-xda
    # efi 分区
    - device: disk-xda
      size: $size_efi
      number: 1
      preserve: true
      grub_device: true
      type: partition
      id: partition-efi
    - fstype: fat32
      volume: partition-efi
      type: format
      id: format-efi
    # os 分区
    - device: disk-xda
      size: $size_os
      number: 2
      preserve: true
      type: partition
      id: partition-os
    - fstype: ext4
      volume: partition-os
      type: format
      id: format-os
    # mount
    - path: /
      device: format-os
      type: mount
      id: mount-os
    - path: /boot/efi
      device: format-efi
      type: mount
      id: mount-efi
EOF
    else
        # bios > 2t
        size_biosboot=$(parted "/dev/$xda" unit b print | grep bios_grub | awk '{print $4}' | sed 's/B$//')
        cat <>/autoinstall.yaml
  config:
    # disk
    - ptable: gpt
      path: /dev/$xda
      preserve: true
      grub_device: true
      type: disk
      id: disk-xda
    # biosboot 分区
    - device: disk-xda
      size: $size_biosboot
      number: 1
      preserve: true
      type: partition
      id: partition-biosboot
    # os 分区
    - device: disk-xda
      size: $size_os
      number: 2
      preserve: true
      type: partition
      id: partition-os
    - fstype: ext4
      volume: partition-os
      type: format
      id: format-os
    # mount
    - path: /
      device: format-os
      type: mount
      id: mount-os
EOF
    fi
else
    # bios
    cat <>/autoinstall.yaml
  config:
    # disk
    - ptable: msdos
      path: /dev/$xda
      preserve: true
      grub_device: true
      type: disk
      id: disk-xda
    # os 分区
    - device: disk-xda
      size: $size_os
      number: 1
      preserve: true
      type: partition
      id: partition-os
    - fstype: ext4
      volume: partition-os
      type: format
      id: format-os
    # mount
    - path: /
      device: format-os
      type: mount
      id: mount-os
EOF
fi
echo ... >>/autoinstall.yaml


================================================
FILE: ubuntu.yaml
================================================
#cloud-config
# 顺序 early-commands > 安装系统 > late-commands > 重启进入系统 > cloud-init: runcmd > cloud-init: 其他
autoinstall:
  version: 1
  apt:
    fallback: offline-install
  source:
    id: "@SOURCE_ID@"
  kernel:
    package: linux-generic
  timezone: Asia/Shanghai
  ssh:
    allow-pw: true
    authorized-keys: []
    install-server: true
  early-commands:
    - |
      # 解决 20.04 不能识别硬盘
      # https://askubuntu.com/questions/1302392/ubuntu-server-20-04-setup-stuck-at-block-probing-did-not-discover-any-disks
      mount | grep /isodevice && { losetup -d /dev/loop0; umount -l /isodevice; } || true

      # 提取 extra_confhome extra_kernel
      prefix=extra
      for var in $(grep -o "\b${prefix}_[^ ]*" /proc/cmdline | xargs); do
          eval "$(echo $var | sed -E "s/${prefix}_([^=]*)=(.*)/\1='\2'/")"
      done

      # 生成分区信息
      xda=$(curl -L "$confhome/get-xda.sh" | sh -s)
      export xda
      curl -L "$confhome/ubuntu-storage-early.sh" | sh -s

      # 要安装的版本
      # 有的镜像只有一个版本,没有 install-sources.yaml
      # 因此提取不到 $source_id,此时 $source_id 参数为空
      if [ -n "$source_id" ]; then
          sed -i "s/@SOURCE_ID@/$source_id/" /autoinstall.yaml
      else
          sed -i "/@SOURCE_ID@/d" /autoinstall.yaml
      fi

      # 内核风味
      # https://bugs.launchpad.net/subiquity/+bug/1989353
      sed -i "s/generic/$kernel/" /run/kernel-meta-package
      sed -i "/package:/s/generic/$kernel/" /autoinstall.yaml

      # 跳过最后的更新
      cp /usr/sbin/chroot /usr/sbin/chroot.bin
      cat >/usr/sbin/chroot <>/etc/hosts
      # sed -i -E 's/(^nameserver )/#\1/' /etc/resolv.conf
  late-commands:
    - |
      # root ssh 登录
      echo "PermitRootLogin yes" >/target/etc/ssh/sshd_config.d/01-permitrootlogin.conf

      # 还原 DNS
      # sed -i -E 's/^#(nameserver )/\1/' /etc/resolv.conf

      # 提取 extra_confhome
      prefix=extra
      for var in $(grep -o "\b${prefix}_[^ ]*" /proc/cmdline | xargs); do
          eval "$(echo $var | sed -E "s/${prefix}_([^=]*)=(.*)/\1='\2'/")"
      done

      # 下载合并分区脚本
      cd /target
      curl -LO $confhome/resize.sh

      # 升级 cloud-init
      # curtin in-target --target=/target -- apt update
      # curtin in-target --target=/target -- apt install --only-upgrade cloud-init
  user-data:
    runcmd:
      - |
        # 合并分区
        bash /resize.sh
    disable_root: false
    users:
      - name: root
        lock_passwd: false
    chpasswd:
      expire: false
      # 20.04 arm 需要
      list: |
        root:123@@@
      users:
        - name: root
          password: 123@@@
          type: text


================================================
FILE: windows-allow-ping.bat
================================================
@echo off
mode con cp select=437 >nul
setlocal EnableDelayedExpansion

rem https://learn.microsoft.com/troubleshoot/windows-server/networking/netsh-advfirewall-firewall-control-firewall-behavior#command-example-4-configure-icmp-settings
rem 旧版命令 netsh firewall set icmpsetting 8 对应的配置是:文件和打印机共享(回显请求 - ICMPv4-In)

set ICMPv4EchoTypeNum=8
set ICMPv6EchoTypeNum=128

for %%i in (4, 6) do (
    netsh advfirewall firewall add rule ^
        name="ICMP Echo Request (ICMPv%%i-In)" ^
        dir=in ^
        action=allow ^
        program=System ^
        protocol=ICMPv%%i:!ICMPv%%iEchoTypeNum!,any
)

rem 删除此脚本
del "%~f0"


================================================
FILE: windows-change-rdp-port.bat
================================================
@echo off
mode con cp select=437 >nul

rem set RdpPort=3333

rem https://learn.microsoft.com/windows-server/remote/remote-desktop-services/clients/change-listening-port
rem HKLM\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\FirewallRules

rem RemoteDesktop-Shadow-In-TCP
rem v2.33|Action=Allow|Active=TRUE|Dir=In|Protocol=6|App=%SystemRoot%\system32\RdpSa.exe|Name=@FirewallAPI.dll,-28778|Desc=@FirewallAPI.dll,-28779|EmbedCtxt=@FirewallAPI.dll,-28752|Edge=TRUE|Defer=App|

rem RemoteDesktop-UserMode-In-TCP
rem v2.33|Action=Allow|Active=TRUE|Dir=In|Protocol=6|LPort=3389|App=%SystemRoot%\system32\svchost.exe|Svc=termservice|Name=@FirewallAPI.dll,-28775|Desc=@FirewallAPI.dll,-28756|EmbedCtxt=@FirewallAPI.dll,-28752|

rem RemoteDesktop-UserMode-In-UDP
rem v2.33|Action=Allow|Active=TRUE|Dir=In|Protocol=17|LPort=3389|App=%SystemRoot%\system32\svchost.exe|Svc=termservice|Name=@FirewallAPI.dll,-28776|Desc=@FirewallAPI.dll,-28777|EmbedCtxt=@FirewallAPI.dll,-28752|

rem 设置端口
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" /v PortNumber /t REG_DWORD /d %RdpPort% /f

rem 设置防火墙
rem 各个版本的防火墙自带的 rdp 规则略有不同
rem 全部版本都有: program=%SystemRoot%\system32\svchost.exe service=TermService
rem win7 还有:    program=System                            service=
rem 以下为并集
for %%a in (TCP, UDP) do (
    netsh advfirewall firewall add rule ^
        name="Remote Desktop - Custom Port (%%a-In)" ^
        dir=in ^
        action=allow ^
        service=any ^
        protocol=%%a ^
        localport=%RdpPort%
)

rem 家庭版没有 rdp 服务
sc query TermService
if %errorlevel% == 1060 goto :del

rem 重启服务 可以用 sc 或者 net
rem UmRdpService 依赖 TermService
rem sc stop 不能处理依赖关系,因此 sc stop TermService 前需要 sc stop UmRdpService
rem net stop 可以处理依赖关系
rem sc stop 是异步的,net stop 不是异步,但有 timeout 时间
rem TermService 运行后,UmRdpService 会自动运行

rem 如果刚好系统在启动 rdp 服务,则会失败,因此要用 goto 循环
rem The Remote Desktop Services service could not be stopped.

rem 有的机器会死循环,开机 logo 不断转圈
rem 通过 netstat netstat -ano 可以看到端口已修改成功,但rdp服务不断重启 (pid一直改变)
rem 因此限定重试次数避免死循环

set retryCount=5

:restartRDP
if %retryCount% LEQ 0 goto :del
net stop TermService /y && net start TermService || (
    set /a retryCount-=1
    timeout 10
    goto :restartRDP
)

:del
del "%~f0"


================================================
FILE: windows-del-gpo.bat
================================================
@echo off
mode con cp select=437 >nul
setlocal enabledelayedexpansion

set "files[1]=%windir%\System32\GroupPolicy\gpt.ini"
set "files[2]=%windir%\System32\GroupPolicy\Machine\Scripts\scripts.ini"

for %%i in (1 2) do (
    set "ini=!files[%%i]!"
    if exist "!ini!.orig" (
        move /y "!ini!.orig" "!ini!"
    ) else (
        del "!ini!"
    )
)

del "%~f0"


================================================
FILE: windows-driver-utils.sh
================================================
#!/bin/ash
# shellcheck shell=dash
# shellcheck disable=SC3001,SC3003,SC3010
# reinstall.sh / trans.sh 共用此文件

# grep 无法处理 UTF-16LE 编码的 inf,有以下几种解决方法
# 1. 使用 ripgrep (rg) 或者 ugrep,但 cygwin 没有
# 2. grep -a a.b.c.d
# 3. iconv -f UTF-16 -t UTF-8

del_inf_comment() {
    sed 's/;.*//'
}

simply_inf() {
    convert_file_to_utf8 "$1" | del_cr | del_inf_comment | trim | del_empty_lines
}

convert_file_to_utf8() {
    # ash 用 * 比较字符串有问题
    # [[ $'\xEF\xBB' = $'\xEF\xBB*' ]] && echo 1

    # UTF-16LE without BOM 要处理吗? windows 支持这种编码的 inf?

    # UTF-16LE with BOM
    if [ "$(head -c2 "$1")" = $'\xFF\xFE' ]; then
        # -f UTF-16LE -t UTF-8 会添加 UTF-8 BOM
        iconv -f UTF-16 -t UTF-8 "$1"

    # UTF-8 with BOM
    elif [ "$(head -c3 "$1")" = $'\xEF\xBB\xBF' ]; then
        # busybox sed 不支持
        # sed '1s/^\xEF\xBB\xBF//' "$1"
        tail -c +4 "$1"

    # 其它
    else
        cat "$1"
    fi
}

simply_inf_word() {
    # 1 删除引号 "
    # 2 删除两边空格
    # 3 \ 和 .\ 换成 /
    # 4 连续的 / 替换成单个 /
    # 5 删除最前面的 /
    sed -E \
        -e 's,",,g' \
        -e 's/^[[:space:]]+//' -e 's/[[:space:]]+$//' \
        -e 's,\.?\\,/,g' \
        -e 's,/+,/,g' \
        -e 's,^/,,'
}

# reinstall.sh 下无法判断 iso 是 32 位还是 64 位,此时 mix_x86_x86_64 为 true
# trans.sh 下可以判断 iso 是 32 位还是 64 位,此时 mix_x86_x86_64 为 false
list_files_from_inf() {
    local inf=$1
    local arch=$2 # x86 amd64 arm64
    local mix_x86_x86_64=$3

    # 所有字段不区分大小写
    inf_txts=$(simply_inf "$inf" | to_lower)

    is_match_section() {
        local section=$1

        [ "$line" = "[$section]" ] || [ "$line" = "[$section.$arch]" ] ||
            { $mix_x86_x86_64 && [ "$arch" = x86 ] && [ "$line" = "[$section.amd64]" ]; } ||
            { $mix_x86_x86_64 && [ "$arch" = amd64 ] && [ "$line" = "[$section.x86]" ]; }
    }

    is_match_catalogfile() {
        local left
        left=$(echo "$line" | awk -F= '{print $1}' | simply_inf_word)

        # catalogfile.nt 是指所有 nt ?
        [ "$left" = "catalogfile" ] ||
            [ "$left" = "catalogfile.nt" ] ||
            [ "$left" = "catalogfile.nt$arch" ] ||
            { $mix_x86_x86_64 && [ "$arch" = x86 ] && [ "$left" = "catalogfile.ntamd64" ]; } ||
            { $mix_x86_x86_64 && [ "$arch" = amd64 ] && [ "$left" = "catalogfile.ntx86" ]; }
    }

    is_match_manufacturer_arch() {
        # x86 可写 NT / NTx86, 其它必须明确架构
        # https://learn.microsoft.com/en-us/windows-hardware/drivers/install/inf-manufacturer-section
        case "$arch" in
        x86) $mix_x86_x86_64 && regex='NT|NTx86|NTamd64' || regex='NT|NTx86' ;;
        amd64) $mix_x86_x86_64 && regex='NT|NTx86|NTamd64' || regex='NTamd64' ;;
        arm64) regex='NTarm64' ;;
        esac

        # 注意 cut awk 结果不同
        # 虽然在这里不会造成影响
        # echo 1 | cut -d, -f2-
        # 1
        # echo 1 | awk -F, '{print $2}'
        # 空白

        echo "$line" | awk -F, '{for(i=2;i<=NF;i++) print $i}' | grep -Eiwq "$regex"
    }

    # 还需要从 [Strings] 读取字符串?

    # 0. 检测 inf 是否适合当前架构
    # 目前没有对比版本号

    ##############################################
    # 注意这种情况, NTamd64.6.0 为空,表示不支持 6.0

    # [Manufacturer]
    # %V_INTEL%   = Intel, NTamd64.6.0, NTamd64.6.1.1

    # [Intel.NTamd64.6.0]
    # ; Empty section.

    # 如果后期改成不从 Manufacturer 获取支持的架构,而是从[]获取支持的架构,注意这种情况
    # [Intel.NTamd64.10.0.1..22000]
    ##############################################

    # 例子1
    # [Manufacturer]
    # %Amazon% = AWSNVME, NTamd64, NTARM64

    # 例子2
    # [Manufacturer]
    # %MyName% = MyName,NTx86.6.0,NTx86.5.1,
    # .
    # [MyName.NTx86.6.0] ; Empty section, so this INF does not support
    # .                  ; NT 6.0 and later.
    # .
    # [MyName.NTx86.5.1] ; Used for NT 5.1 and later
    # .                  ; (but not NT 6.0 and later due to the NTx86.6.0 entry)
    # %MyDev% = InstallB,hwid
    # .
    # [MyName]           ; Empty section, so this INF does not support
    # .                  ; Win2000
    # .

    # 例子3
    # 系统自带的驱动,没有 [Manufacturer]

    # 例子4
    # C:\Windows\INF\wfcvsc.inf
    # %StdMfg%=Standard,NTamd64...0x0000001,NTamd64...0x0000002,NTamd64...0x0000003

    in_section=false
    arch_matched=false
    has_manufacturer=false
    # 未添加 IFS= 时,read 会删除行首行尾的空白字符
    while read -r line; do
        if [[ "$line" = "["* ]]; then
            is_match_section manufacturer && has_manufacturer=true && in_section=true || in_section=false
            continue
        fi

        if $in_section; then
            if is_match_manufacturer_arch; then
                arch_matched=true
                break
            fi
        fi
    done < <(echo "$inf_txts")

    if $has_manufacturer && ! $arch_matched; then
        return 10
    fi

    # 1. 输出 .inf 文件名
    basename "$inf"

    # 2. 输出 .cat 相对路径
    # 例子
    # [version]
    # CatalogFile = "xxxxx.cat"
    # CatalogFile.NTAMD64=Balloon.cat
    in_section=false
    # 未添加 IFS= 时,read 会删除行首行尾的空白字符
    while read -r line; do
        if [[ "$line" = "["* ]]; then
            is_match_section version && in_section=true || in_section=false
            continue
        fi

        if $in_section && is_match_catalogfile; then
            echo "$line" | awk -F= '{print $2}' | simply_inf_word
        fi
    done < <(echo "$inf_txts")

    # 3. 获取 SourceDisksNames
    # 例子
    # [SourceDisksNames]
    # 1 = "Windows NT CD-ROM",file.tag,, "\common"
    SourceDisksNames=
    in_section=false
    # 未添加 IFS= 时,read 会删除行首行尾的空白字符
    while read -r line; do
        if [[ "$line" = "["* ]]; then
            is_match_section sourcedisksnames && in_section=true || in_section=false
            continue
        fi
        # 注意可能有空格和引号

        if $in_section; then
            local num dir
            num=$(echo "$line" | awk -F= '{print $1}' | simply_inf_word)
            dir=$(echo "$line" | awk -F, '{print $4}' | simply_inf_word)
            # 每行一条记录
            if [ -n "$SourceDisksNames" ]; then
                SourceDisksNames=$SourceDisksNames$'\n'
            fi
            SourceDisksNames="$SourceDisksNames$num:$dir"
        fi
    done < <(echo "$inf_txts")

    # 4. 打印 SourceDisksFiles 的绝对路径
    # 例子
    # [SourceDisksFiles]
    # aha154x.sys = 1 , "\x86" ,,
    in_section=false
    # 未添加 IFS= 时,read 会删除行首行尾的空白字符
    while read -r line; do
        if [[ "$line" = "["* ]]; then
            is_match_section sourcedisksfiles && in_section=true || in_section=false
            continue
        fi

        if $in_section; then
            file=$(echo "$line" | awk -F= '{print $1}' | simply_inf_word)
            num=$(echo "$line" | awk -F'=|,' '{print $2}' | simply_inf_word)
            sub_dir=$(echo "$line" | awk -F, '{print $2}' | simply_inf_word)
            # 可能有多个
            while IFS= read -r parent_dir; do
                echo "$parent_dir/$sub_dir/$file" | simply_inf_word
            done < <(echo "$SourceDisksNames" | awk -F: "\$1==\"$num\" {print \$2}")
        fi
    done < <(echo "$inf_txts")
}

# windows 安装驱动时,只会安装相同架构的驱动文件到系统,即使 inf 里有列出其它架构的驱动
# 因此 DISM 导出驱动时,也就没有包含其它架构的驱动文件

# 用于尽可能匹配路径大小写
get_path_in_correct_case() {
    # 同时支持参数和管道
    local path
    path=$({ if [ -n "$1" ]; then echo "$1"; else cat; fi; })

    # 用 / 分割路径,提取成列表
    # 例如: ///a///b/c.inf -> a b c.inf
    # shellcheck disable=SC2046
    set -- $(echo "$path" | grep -o '[^/]*')
    (
        local output=
        if is_absolute_path "$path"; then
            cd /
            output=/
        fi

        stop_find=false

        while [ $# -gt 0 ]; do
            local part=$1
            local tmp
            # shellcheck disable=SC2010
            if ! $stop_find; then
                if tmp=$(ls -1 | grep -Fix "$part"); then
                    part=$tmp
                    # 大于 1 表示当前 part 是目录
                    if [ $# -gt 1 ]; then
                        if ! cd "$part" 2>/dev/null; then
                            warn "Can't cd $path"
                            stop_find=true
                        fi
                    fi
                else
                    stop_find=true
                fi
            fi

            if [ $# -gt 1 ]; then
                output="$output$part/"
            else
                # 最后 part
                output="$output$part"
            fi
            shift
        done

        echo "$output"
    )
}

is_file_or_link() {
    # -e / -f 坏软连接,返回 false
    # -L 坏软连接,返回 true
    [ -f "$1" ] || [ -L "$1" ]
}

find_file_ignore_case() {
    # 同时支持参数和管道
    local path
    path=$({ if [ -n "$1" ]; then echo "$1"; else cat; fi; })

    path=$(get_path_in_correct_case "$path")
    if is_file_or_link "$path"; then
        echo "$path"
    else
        warn "Can't find $path" >&2
        return 1
    fi
}

parse_inf_and_cp_driever() {
    local inf=$1
    local dst=$2
    local arch=$3
    local mix_x86_x86_64=$4

    info false "Add driver: $inf"

    # 首先创建目录,否则无法通过 ls 文件数得到编号
    mkdir -p "$dst"
    # shellcheck disable=SC2012
    inf_index=$(($(ls -1 "$dst" | wc -l) + 1))
    inf_old_dir=$(dirname "$inf")
    inf_new_dir=$dst/$inf_index
    if driver_files=$(list_files_from_inf "$inf" "$arch" "$mix_x86_x86_64"); then
        mkdir -p "$inf_new_dir"
        (
            cd "$inf_old_dir" || error_and_exit "Can't cd $inf_old_dir"
            while read -r file; do
                if file=$(find_file_ignore_case "$file"); then
                    cp -v --parents "$file" "$inf_new_dir"
                fi
            done < <(echo "$driver_files")
        )
    else
        if [ $? -eq 10 ]; then
            warn "$inf arch not match."
        else
            error_and_exit "Unknown error while parse $inf."
        fi
    fi
}

get_sys_dir_for_eth() {
    (
        cd "$(readlink -f "/sys/class/net/$1")" || error_and_exit "Can't cd to $1"
        while ! [ "$(pwd)" = / ]; do
            # DRIVER=virtio-pci
            # PCI_CLASS=20000            # 2 开头表示网络设备
            # PCI_ID=1AF4:1041
            # PCI_SUBSYS_ID=1AF4:1100
            # PCI_SLOT_NAME=0000:03:00.0
            if [ -f uevent ] && grep -q 'PCI_CLASS=2' uevent && grep -q 'PCI_ID' uevent; then
                pwd
                return
            fi
            cd ..
        done
        return 1
    )
}


================================================
FILE: windows-frpc.bat
================================================
@echo off
mode con cp select=437 >nul

rem Windows Deferder 会误报,因此要添加白名单
powershell -ExecutionPolicy Bypass -Command "Add-MpPreference -ExclusionPath '%SystemDrive%\frpc\frpc.exe'"

rem 启用日志
rem wevtutil set-log Microsoft-Windows-TaskScheduler/Operational /enabled:true

rem 创建计划任务并立即运行
schtasks /Create /TN "frpc" /XML "%SystemDrive%\frpc\frpc.xml"
schtasks /Run /TN "frpc"
del "%SystemDrive%\frpc\frpc.xml"

rem win10+ 在用户首次登录后,用 LocalService 用户运行的计划任务才会生效
rem 即使手动重启,计划任务也没有运行

rem 如果 10 秒内有 frpc 进程,则代表计划任务已经生效,不需要首次登录
rem 如果 10 秒后也没有 frpc 进程,则需要临时改用 SYSTEM 用户运行计划任务
for /L %%i in (1,1,10) do (
    timeout 1
    tasklist /FI "IMAGENAME eq frpc.exe" | find /I "frpc.exe" && (
        goto :end
    )
)

rem 临时改用 SYSTEM 用户运行计划任务
schtasks /Change /TN frpc /RU S-1-5-18
schtasks /Run /TN frpc

rem 用户登录后改回用 LocalService 运行
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" /f ^
    /v FrpcRunAsLocalService ^
    /t REG_SZ ^
    /d "schtasks /Change /TN frpc /RU S-1-5-19"

:end
rem 删除此脚本
del "%~f0"


================================================
FILE: windows-resize.bat
================================================
@echo off
mode con cp select=437 >nul

set C=%SystemDrive:~0,1%
for /f "tokens=2" %%a in ('echo list vol ^| diskpart ^| findstr "\"') do (echo select vol %%a & echo delete partition) | diskpart
for /f "tokens=2" %%a in ('echo list vol ^| diskpart ^| findstr "\<%C%\>"') do (echo select vol %%a & echo extend) | diskpart
del "%~f0"


================================================
FILE: windows-set-netconf.bat
================================================
rem set mac_addr=11:22:33:aa:bb:cc

rem set ipv4_addr=192.168.1.2/24
rem set ipv4_gateway=192.168.1.1
rem set ipv4_dns1=192.168.1.1
rem set ipv4_dns2=192.168.1.2

rem set ipv6_addr=2222::2/64
rem set ipv6_gateway=2222::1
rem set ipv6_dns1=::1
rem set ipv6_dns2=::2

@echo off
mode con cp select=437 >nul

rem 禁用 IPv6 地址标识符的随机化,防止 IPv6 和后台面板不一致
netsh interface ipv6 set global randomizeidentifiers=disabled

rem 检查是否定义了 MAC 地址
if not defined mac_addr goto :del

rem vista 没有自带 powershell
rem win11 24h2 安装后有 wmic,但是过一段时间会自动删除,因此有的 dd 镜像没有 wmic
if exist "%windir%\system32\wbem\wmic.exe" (
    rem wmic 换行符是 \r\r\n
    rem 虽然这里用了 findstr 全字匹配 ,但是结尾还是有 \r
    for /f "tokens=2 delims==" %%a in (
        'wmic nic where "MACAddress='%mac_addr%'" get InterfaceIndex /format:list ^| findstr "^InterfaceIndex=[0-9][0-9]*$"'
    ) do set id=%%a
)

if not defined id (
    for /f %%a in ('powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass ^
        -Command "(Get-WmiObject Win32_NetworkAdapter | Where-Object { $_.MACAddress -eq '%mac_addr%' }).InterfaceIndex" ^| findstr "^[0-9][0-9]*$"'
    ) do set id=%%a
)

if not defined id (
    for /f %%a in ('powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass ^
        -Command "(Get-CimInstance Win32_NetworkAdapter | Where-Object { $_.MACAddress -eq '%mac_addr%' }).InterfaceIndex" ^| findstr "^[0-9][0-9]*$"'
    ) do set id=%%a
)

if defined id (
    rem 配置静态 IPv4 地址和网关
    if defined ipv4_addr if defined ipv4_gateway (
        rem 如果使用了 setlocal EnableDelayedExpansion
        rem netsh interface ipv4 set address !id! static %ipv4_addr% gateway=%ipv4_gateway% gwmetric=0
        rem !id! 变量最后有 \r 会导致语句不正确
        rem %id% 变量则没有这个问题

        rem gwmetric 默认值为 1,自动跃点需设为 0
        netsh interface ipv4 set address %id% static %ipv4_addr% gateway=%ipv4_gateway% gwmetric=0
    )

    rem 配置静态 IPv4 DNS 服务器
    for %%i in (1, 2) do (
        if defined ipv4_dns%%i (
            netsh interface ipv4 add | findstr "dnsservers" >nul
            if ErrorLevel 1 (
                rem vista
                setlocal EnableDelayedExpansion
                netsh interface ipv4 add dnsserver %id% !ipv4_dns%%i! %%i
                endlocal
            ) else (
                rem win7
                setlocal EnableDelayedExpansion
                netsh interface ipv4 add dnsservers %id% !ipv4_dns%%i! %%i no
                endlocal
            )
        )
    )

    rem 配置 IPv6 地址和网关
    if defined ipv6_addr if defined ipv6_gateway (
        netsh interface ipv6 set address %id% %ipv6_addr%
        netsh interface ipv6 add route prefix=::/0 %id% %ipv6_gateway%
    )

    rem 配置 IPv6 DNS 服务器
    for %%i in (1, 2) do (
        if defined ipv6_dns%%i (
            netsh interface ipv6 add | findstr "dnsservers" >nul
            if ErrorLevel 1 (
                rem vista
                setlocal EnableDelayedExpansion
                netsh interface ipv6 add dnsserver %id% !ipv6_dns%%i! %%i
                endlocal
            ) else (
                rem win7
                setlocal EnableDelayedExpansion
                netsh interface ipv6 add dnsservers %id% !ipv6_dns%%i! %%i no
                endlocal
            )
        )
    )
)

:del
rem 删除此脚本
del "%~f0"


================================================
FILE: windows-setup.bat
================================================
@echo off
mode con cp select=437 >nul

rem 还原 setup.exe
rename X:\setup.exe.disabled setup.exe

rem 等待 10 秒才自动安装
cls
for /l %%i in (10,-1,1) do (
    echo Press Ctrl+C within %%i seconds to cancel the automatic installation.
    call :sleep 1000
    cls
)

rem win7 find 命令在 65001 代码页下有问题,仅限 win 7
rem findstr 就正常,但安装程序又没有 findstr
rem echo a | find "a"

rem 使用高性能模式
rem https://learn.microsoft.com/windows-hardware/manufacture/desktop/capture-and-apply-windows-using-a-single-wim
rem win8 pe 没有 powercfg
powercfg /s SCHEME_MIN 2>nul

rem 安装 SCSI 驱动
if exist X:\drivers\ (
    for /f "delims=" %%F in ('dir /s /b "X:\drivers\*.inf" 2^>nul') do (
        call :drvload_if_scsi "%%~F"
    )

    rem 官网写了可以安装但仅会加载关键驱动
    rem Gcore 的 virtio-gpu 在安装时没有显示
    rem 即使安装时加载了显卡驱动
    rem 进入系统后才有显示
    rem find /i "viogpudo" "%%~F" >nul
    rem if not errorlevel 1 (
    rem     drvload "%%~F"
    rem )
)

rem 安装自定义 SCSI 驱动
rem 可以用 forfiles /p X:\custom_drivers /m *.inf /c "cmd /c echo @path"
rem 不可以用 for %%F in ("X:\custom_drivers\*\*.inf")
if exist X:\custom_drivers\ (
    for /f "delims=" %%F in ('dir /s /b "X:\custom_drivers\*.inf" 2^>nul') do (
        call :drvload_if_scsi "%%~F"
    )
)

rem 等待加载分区
call :sleep 5000
echo rescan | diskpart

rem 判断 efi 还是 bios
rem 或者用 https://learn.microsoft.com/windows-hardware/manufacture/desktop/boot-to-uefi-mode-or-legacy-bios-mode
rem pe 下没有 mountvol
echo list vol | diskpart | find "efi" && (
    set BootType=efi
) || (
    set BootType=bios
)

rem 获取 ProductType
rem for /f "tokens=3" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\ProductOptions" /v ProductType') do (
rem     set "ProductType=%%a"
rem )

rem 获取 BuildNumber
for /f "tokens=3" %%a in ('reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion" /v CurrentBuildNumber') do (
    set "BuildNumber=%%a"
)

rem 获取 installer 卷 id
for /f "tokens=2" %%a in ('echo list vol ^| diskpart ^| find "installer"') do (
    set "VolIndex=%%a"
)

rem 将 installer 分区设为 Y 盘
(echo select vol %VolIndex% & echo assign letter=Y) | diskpart

rem 旧版安装程序会自动在 C 盘设置虚拟内存
rem 新版安装程序(24h2)不会自动设置虚拟内存
rem 在 installer 分区创建虚拟内存,不用白不用
call :createPageFile

rem 查看虚拟内存
rem wmic pagefile

rem 获取主硬盘 id
rem vista pe 没有 wmic,因此用 diskpart
(echo select vol %VolIndex% & echo list disk) | diskpart | find "* Disk " > X:\disk.txt
for /f "tokens=3" %%a in (X:\disk.txt) do (
    set "DiskIndex=%%a"
)
del X:\disk.txt

rem 这个变量会被 trans.sh 修改
set is4kn=0
if "%is4kn%"=="1" (
    set EFISize=260
) else (
    set EFISize=100
)

rem 重新分区/格式化
(if "%BootType%"=="efi" (
    echo select disk %DiskIndex%

    echo select part 1
    echo delete part override
    echo select part 2
    echo delete part override
    echo select part 3
    echo delete part override

    echo create part efi size=%EFISize%
    echo format fs=fat32 quick

    echo create part msr size=16

    echo create part primary
    echo format fs=ntfs quick
    rem echo assign letter=Z
) else (
    echo select disk %DiskIndex%

    echo select part 1
    rem echo delete part override
    rem echo create part primary
    echo format fs=ntfs quick
    echo active
    rem echo assign letter=Z
)) > X:\diskpart.txt

rem 使用 diskpart /s ,出错不会执行剩下的 diskpart 命令
diskpart /s X:\diskpart.txt
del X:\diskpart.txt

rem 盘符
rem X boot.wim (ram)
rem Y installer
rem Z os

rem 旧版安装程序会自动在C盘设置虚拟内存,新版安装程序(24h2)不会
rem 如果不创建虚拟内存,1g 内存的机器安装时会报错/杀进程
if %BuildNumber% GEQ 26040 (
    rem 已经在 installer 分区创建了虚拟内存,约等于 boot.wim 的大小,因此这步不需要
    rem vista/2008 没有删除 boot.wim,200M预留空间-(文件系统占用+驱动占用)后,实测能创建1个64M虚拟内存文件
    rem call :createPageFileOnZ
)

rem 设置应答文件的主硬盘 id
set "file=X:\windows.xml"
set "tempFile=X:\tmp.xml"

set "search=%%disk_id%%"
set "replace=%DiskIndex%"

(for /f "delims=" %%i in (%file%) do (
    set "line=%%i"

    setlocal EnableDelayedExpansion
    echo !line:%search%=%replace%!
    endlocal

)) > %tempFile%
move /y %tempFile% %file%


rem https://github.com/pbatard/rufus/issues/1990
for %%a in (RAM TPM SecureBoot) do (
    reg add HKLM\SYSTEM\Setup\LabConfig /t REG_DWORD /v Bypass%%aCheck /d 1 /f
)

rem 设置
set ForceOldSetup=0
set EnableUnattended=1
set EnableEMS=0

rem 运行 ramdisk X:\setup.exe 的话
rem vista 会找不到安装源
rem server 23h2 会无法运行
rem 使用 /installfrom 可以解决?
if "%ForceOldSetup%"=="1" (
    set setup=Y:\sources\setup.exe
) else (
    set setup=Y:\setup.exe
)

if "%EnableUnattended%"=="1" (
    set Unattended=/unattend:X:\windows.xml
)

rem 新版安装程序默认开了 Compact OS

rem 新版安装程序不会创建 BIOS MBR 引导
rem 因此要回退到旧版,或者手动修复 MBR
rem server 2025 + bios 也是
rem 但是 server 2025 官网写支持 bios
rem TODO: 使用 ms-sys 可以不修复?
if %BuildNumber% GEQ 26040 if "%BootType%"=="bios" (
    rem set ForceOldSetup=1
    bootrec /fixmbr
)

rem 旧版安装程序不会创建 winre 分区
rem 新版安装程序会创建 winre 分区
rem winre 分区创建在 installer 分区前面
rem 禁止 winre 分区后,winre 储存在 C 盘,依然有效
if %BuildNumber% GEQ 26040 if "%ForceOldSetup%"=="0" (
    set ResizeRecoveryPartition=/ResizeRecoveryPartition Disable
)

rem 为 windows server 打开 EMS/SAC
rem 普通 windows 没有自带 SAC 组件,暂不处理
rem 现在通过 trans.sh 准确检测系统是否有 SAC 组件,有则修改 EnableEMS 变量打开 EMS
if "%EnableEMS%"=="1" (
    rem set EMS=/EMSPort:UseBIOSSettings /EMSBaudRate:115200
    set EMS=/EMSPort:COM1 /EMSBaudRate:115200
)

echo on
%setup% %ResizeRecoveryPartition% %EMS% %Unattended%
exit /b

:sleep
rem 没有加载网卡驱动,无法用 ping 来等待
rem 没有 timeout 命令
rem timeout /t 10 /nobreak
echo wscript.sleep(%~1) > X:\sleep.vbs
cscript //nologo X:\sleep.vbs
del X:\sleep.vbs
exit /b

:createPageFile
rem 尽量填满空间,pagefile 默认 64M
for /l %%i in (1, 1, 100) do (
    wpeutil CreatePageFile /path=Y:\pagefile%%i.sys >nul 2>nul && echo Created pagefile%%i.sys || exit /b
)
exit /b

:createPageFileOnZ
wpeutil CreatePageFile /path=Z:\pagefile.sys /size=512
exit /b

:drvload_if_scsi
rem 不要查找 Class=SCSIAdapter 因为有些驱动等号两边有空格
find /i "SCSIAdapter" "%~1" >nul
if not errorlevel 1 (
    drvload "%~1"
)
exit /b


================================================
FILE: windows.xml
================================================



    
        
            
            false
            
                true
                
                    %key%
                
            
            
                
                    
                        
                            /IMAGE/NAME
                            %image_name%
                        
                    
                    
                        %disk_id%
                        %installto_partitionid%
                    
                
            
            
            
            
            
        
        
            %locale%
            %locale%
            %locale%
            %locale%
        
    

    

    
        
            
                
                
                
                    X:\drivers
                
                
                    X:\custom_drivers
                
            
        
    

    
        
            
                
                
                    1
                    powercfg /setacvalueindex SCHEME_BALANCED SUB_VIDEO VIDEOIDLE 0
                
                
                    2
                    powercfg /setacvalueindex SCHEME_MIN SUB_VIDEO VIDEOIDLE 0
                
                
                    3
                    powercfg /setacvalueindex SCHEME_MAX SUB_VIDEO VIDEOIDLE 0
                
                
                
                    4
                    powercfg /setactive SCHEME_MIN
                
                
                
                    5
                    
                    
                    
                    
                    
                    cmd /c "for %a in (Administrator Administrador Administrateur Administratör Администратор Järjestelmänvalvoja Rendszergazda) do (net user %a /active:yes && exit)"
                
                
                
                    6
                    reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager /v ShippedWithReserves /t REG_DWORD /d 0 /f
                    
                    
                    
                
                
                
            
        
        
            false
        
        
            
                
                    all
                    
                    
                    
                    @FirewallAPI.dll,-28752
                    %use_default_rdp_port%
                
            
        
        
            8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
        
        
            1
        
        
            0
        
    

    
        
        
        
            
                
                    %administrator_password%
                    false</PlainText>
                </AdministratorPassword>
            </UserAccounts>
            <OOBE>
                <HideEULAPage>true</HideEULAPage>
                <ProtectYourPC>3</ProtectYourPC>
                <SkipMachineOOBE>true</SkipMachineOOBE>
                <SkipUserOOBE>true</SkipUserOOBE>
            </OOBE>
            <!-- 不设置时区,则使用镜像的默认时区 -->
            <!-- <TimeZone>China Standard Time</TimeZone> -->
        </component>
    </settings>
</unattend>


================================================
FILE: wmic.ps1
================================================
param(
    [string]$Namespace,
    [string]$Class,
    [string]$Filter,
    [string]$Properties
)

$propertiesToDisplay = if ($Properties) { $Properties.Split(",") } else { @("*") }

$wmiQuery = @{
    Namespace = $Namespace
    Class     = $Class
}

if ($Filter) {
    $wmiQuery.Filter = $Filter
}

Get-WmiObject @wmiQuery | ForEach-Object {
    $_.PSObject.Properties | Where-Object {
        -not $_.Name.StartsWith("__") -and
        ($propertiesToDisplay -contains $_.Name -or $propertiesToDisplay -contains "*")
    } | ForEach-Object {
        $name = $_.Name
        $value = $_.Value

        # 改成 wmic 的输出格式
        if ($value -is [Array]) {
            $formattedValue = ($value | ForEach-Object { "`"$_`"" }) -join ","
            Write-Output "$name={$formattedValue}"
        }
        else {
            Write-Output "$name=$value"
        }
    }
}