[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = space\nindent_size = 4\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[{windows.xml,windows-*.xml}]\nend_of_line = crlf\n\n[windows-frpc.xml]\ncharset = utf-16-le\nindent_size = 2\n\n[*.{bat,cmd,ps1}]\nend_of_line = crlf\n\n[*.ps1]\ncharset = utf-8-bom\n\n[*.{yml,yaml}]\nindent_size = 2\n\n[reinstall.sh]\nshell_variant = bash\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: bin456789\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report (问题反馈)\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n原来的系统 (Original system):\n\n要安装的系统 (System to be installed):\n\n遇到的问题 (Issue):\n\n<!--\n请上传截图或者报错内容，注意删除 IP 地址和密码\n(Please Upload Screenshot or error message. Be sure to delete the IP address and password)\n-->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request (功能请求)\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n\n"
  },
  {
    "path": ".github/workflows/run_reinstall.yml",
    "content": "name: 运行主程序\non:\n  workflow_dispatch:\n  push:\n    paths:\n      - \"reinstall.*\"\njobs:\n  run:\n    name: 运行主程序\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest]\n        include:\n          - os: ubuntu-latest\n            command: sudo bash reinstall.sh --debug --password 123@@@\n          - os: windows-latest\n            command: ./reinstall.bat --debug --password 123@@@\n    runs-on: ${{ matrix.os }}\n    steps:\n      - run: |\n          git config --global core.autocrlf false\n      - uses: actions/checkout@v4\n      - run: |\n          # ${{ matrix.command }} centos\n          ${{ matrix.command }} almalinux\n          # ${{ matrix.command }} rocky\n          # ${{ matrix.command }} fedora\n          # ${{ matrix.command }} oracle\n          ${{ matrix.command }} ubuntu\n          ${{ matrix.command }} debian\n          ${{ matrix.command }} debian --ci\n          # ${{ matrix.command }} kali\n          # ${{ matrix.command }} alpine\n          # ${{ matrix.command }} opensuse\n          # ${{ matrix.command }} arch\n          # ${{ matrix.command }} gentoo\n\n          ${{ matrix.command }} netboot.xyz\n          ${{ matrix.command }} dd --img=https://download.opensuse.org/tumbleweed/appliances/openSUSE-MicroOS.x86_64-SelfInstall.raw.xz\n          ${{ matrix.command }} windows --image-name='Windows Server blah' --iso https://aka.ms/HCIReleaseImage\n\n          ${{ matrix.command }} reset\n\n          # 测试失败例子\n          # ${{ matrix.command }} wrong-os\n          # ${{ matrix.command }} dd --img=https://github.com/\n          # ${{ matrix.command }} windows --iso=https://github.com/ --image-name=abc\n"
  },
  {
    "path": ".github/workflows/sync_to_cnb.yml",
    "content": "name: 同步到 CNB\non:\n  workflow_dispatch:\n  push:\njobs:\n  run:\n    name: 同步到 CNB\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - uses: yesolutions/mirror-action@master\n        with:\n          REMOTE: https://cnb.cool/bin456789/reinstall.git\n          GIT_USERNAME: cnb\n          GIT_PASSWORD: ${{ secrets.CNB_TOKEN }}\n          PUSH_ALL_REFS: false\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.en.md",
    "content": "<!-- markdownlint-disable MD028 MD033 MD045 -->\n\n# reinstall\n\n[![Codacy](https://img.shields.io/codacy/grade/dc679a17751448628fe6d8ac35e26eed?logo=Codacy&label=Codacy&style=flat-square)](https://app.codacy.com/gh/bin456789/reinstall/dashboard)\n[![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)\n[![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)\n<!-- [![Lines of Code](https://tokei.rs/b1/github/bin456789/reinstall?category=code&label=Lines%20of%20Code&style=flat-square)](https://github.com/XAMPPRocky/tokei_rs) -->\n\nOne-Click system reinstallation script for VPS [中文](README.md)\n\n## Introduction\n\n- One-click reinstallation to Linux: Supports 19 common distributions.\n- 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`.\n- Supports reinstallation in any direction, i.e., `Linux to Linux`, `Linux to Windows`, `Windows to Windows`, `Windows to Linux`\n- Automatically configures IP and intelligently sets it as static or dynamic. Supports `/32`, `/128`, `gateway outside subnet`, `IPv6 only`, `IPv4/IPv6 on different NIC`\n- Specially optimized for low-spec servers, requires less memory than the official netboot\n- Uses partition table ID to identify hard drives throughout the process, ensuring no wrong disk is written\n- Supports BIOS and EFI boot, and ARM Server\n- No homemades image included, all resources are obtained in real-time from mirror sites\n\nIf this helped you, you can buy me a milk tea.\n[![Donate](https://img.shields.io/badge/Donate-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#EA4AAA)](https://github.com/sponsors/bin456789)\n\n[![Sponsors](https://raw.githubusercontent.com/bin456789/sponsors/refs/heads/master/sponsors.svg)](https://github.com/sponsors/bin456789)\n\n### Feedback\n\n[![GitHub Issues](https://img.shields.io/badge/GitHub-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/bin456789/reinstall/issues)\n[![Telegram Group](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/reinstall_os)\n\n## Quick Start\n\n- [Download](#download-current-system-is--linux)\n- [Feature 1. One-click reinstallation to Linux](#feature-1-install--linux)\n- [Feature 2. One-click DD Raw image to hard disk](#feature-2-dd-raw-image-to-hard-disk)\n- [Feature 3. One-click reboot to Alpine Live OS](#feature-3-reboot-to--alpine-live-os)\n- [Feature 4. One-click reboot to netboot.xyz](#feature-4-reboot-to--netbootxyz)\n- [Feature 5. One-click reinstallation to Windows](#feature-5-install--windows-iso)\n- [Cancel the reinstallation](#cancel-the-reinstallation)\n\n## System Requirements\n\nThe original system can be any system listed in the table.\n\nThe system requirements for the target system are as follows:\n\n| System                                                                                                                                                                                                                                                                                                                                                                 | Version                               | Memory    | Disk             |\n| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | --------- | ---------------- |\n| <img width=\"16\" height=\"16\" src=\"https://www.alpinelinux.org/alpine-logo.ico\" /> Alpine                                                                                                                                                                                                                                                                                | 3.20, 3.21, 3.22, 3.23                | 256 MB    | 1 GB             |\n| <img width=\"16\" height=\"16\" src=\"https://www.debian.org/favicon.ico\" /> Debian                                                                                                                                                                                                                                                                                         | 9, 10, 11, 12, 13                     | 256 MB    | 1 ~ 1.5 GB ^     |\n| <img width=\"16\" height=\"16\" src=\"https://github.com/bin456789/reinstall/assets/7548515/f74b3d5b-085f-4df3-bcc9-8a9bd80bb16d\" /> Kali                                                                                                                                                                                                                                   | Rolling                               | 256 MB    | 1 ~ 1.5 GB ^     |\n| <img width=\"16\" height=\"16\" src=\"https://documentation.ubuntu.com/server/_static/favicon.png\" /> Ubuntu                                                                                                                                                                                                                                                                | 16.04 LTS - 24.04 LTS, 25.10          | 512 MB \\* | 2 GB             |\n| <img width=\"16\" height=\"16\" src=\"https://img.alicdn.com/imgextra/i1/O1CN01oJnJZg1yK4RzI4Rx2_!!6000000006559-2-tps-118-118.png\" /> Anolis                                                                                                                                                                                                                               | 7, 8, 23                              | 512 MB \\* | 5 GB             |\n| <img width=\"16\" height=\"16\" src=\"https://www.redhat.com/favicon.ico\" /> RHEL &nbsp;<img width=\"16\" height=\"16\" src=\"https://almalinux.org/fav/favicon.ico\" /> AlmaLinux &nbsp;<img width=\"16\" height=\"16\" src=\"https://rockylinux.org/favicon.png\" /> Rocky &nbsp;<img width=\"16\" height=\"16\" src=\"https://www.oracle.com/asset/web/favicons/favicon-32.png\" /> Oracle | 8, 9, 10                              | 512 MB \\* | 5 GB             |\n| <img width=\"16\" height=\"16\" src=\"https://opencloudos.org/qq.ico\" /> OpenCloudOS                                                                                                                                                                                                                                                                                        | 8, 9, Stream 23                       | 512 MB \\* | 5 GB             |\n| <img width=\"16\" height=\"16\" src=\"https://www.centos.org/assets/icons/favicon.svg\" /> CentOS Stream                                                                                                                                                                                                                                                                     | 9, 10                                 | 512 MB \\* | 5 GB             |\n| <img width=\"16\" height=\"16\" src=\"https://fedoraproject.org/favicon.ico\" /> Fedora                                                                                                                                                                                                                                                                                      | 42, 43                                | 512 MB \\* | 5 GB             |\n| <img width=\"16\" height=\"16\" src=\"https://www.openeuler.org/favicon.ico\" /> openEuler                                                                                                                                                                                                                                                                                   | 20.03 LTS - 24.03 LTS, 25.09          | 512 MB \\* | 5 GB             |\n| <img width=\"16\" height=\"16\" src=\"https://static.opensuse.org/favicon.ico\" /> openSUSE                                                                                                                                                                                                                                                                                  | Leap 15.6, 16.0, Tumbleweed (Rolling) | 512 MB \\* | 5 GB             |\n| <img width=\"16\" height=\"16\" src=\"https://nixos.org/favicon.svg\" /> NixOS                                                                                                                                                                                                                                                                                               | 25.11                                 | 512 MB    | 5 GB             |\n| <img width=\"16\" height=\"16\" src=\"https://archlinux.org/static/favicon.png\" /> Arch                                                                                                                                                                                                                                                                                     | Rolling                               | 512 MB    | 5 GB             |\n| <img width=\"16\" height=\"16\" src=\"https://www.gentoo.org/assets/img/logo/gentoo-g.png\" /> Gentoo                                                                                                                                                                                                                                                                        | Rolling                               | 512 MB    | 5 GB             |\n| <img width=\"16\" height=\"16\" src=\"https://aosc.io/distros/aosc-os.svg\" /> AOSC OS                                                                                                                                                                                                                                                                                       | Rolling                               | 512 MB    | 5 GB             |\n| <img width=\"16\" height=\"16\" src=\"https://www.fnnas.com/favicon.ico\" /> fnOS                                                                                                                                                                                                                                                                                            | 1                                     | 512 MB    | 8 GB             |\n| <img width=\"16\" height=\"16\" src=\"https://blogs.windows.com/wp-content/uploads/prod/2022/09/cropped-Windows11IconTransparent512-32x32.png\" /> Windows (DD)                                                                                                                                                                                                              | Any                                   | 512 MB    | Depends on image |\n| <img width=\"16\" height=\"16\" src=\"https://blogs.windows.com/wp-content/uploads/prod/2022/09/cropped-Windows11IconTransparent512-32x32.png\" /> Windows (ISO)                                                                                                                                                                                                             | Vista, 7, 8.x (Server 2008 - 2012 R2) | 512 MB    | 25 GB            |\n| <img width=\"16\" height=\"16\" src=\"https://blogs.windows.com/wp-content/uploads/prod/2022/09/cropped-Windows11IconTransparent512-32x32.png\" /> Windows (ISO)                                                                                                                                                                                                             | 10, 11 (Server 2016 - 2025)           | 1 GB      | 25 GB            |\n\n\\* Indicates installation using cloud images, not traditional network installation.\n\n^ Indicates requiring either 256 MB memory + 1.5 GB disk, or 512 MB memory + 1 GB disk\n\n> [!WARNING]\n>\n> In theory it also supports dedicated servers and PCs\n>\n> but if you can use IPMI or a USB drive, this script is not recommended.\n\n> [!WARNING]\n>\n> ❌ This script does not support OpenVZ or LXC virtual machines.\n>\n> Please use <https://github.com/LloydAsp/OsMutation> instead.\n\n## Download (Current system is <img width=\"20\" height=\"20\" src=\"https://www.kernel.org/theme/images/logos/favicon.png\" /> Linux)\n\nFor server outside China:\n\n```bash\ncurl -O https://raw.githubusercontent.com/bin456789/reinstall/main/reinstall.sh || wget -O ${_##*/} $_\n```\n\nFor server inside China:\n\n```bash\ncurl -O https://cnb.cool/bin456789/reinstall/-/git/raw/main/reinstall.sh || wget -O ${_##*/} $_\n```\n\n## Download (Current system is <img width=\"20\" height=\"20\" src=\"https://blogs.windows.com/wp-content/uploads/prod/2022/09/cropped-Windows11IconTransparent512-32x32.png\" /> Windows)\n\n> [!IMPORTANT]\n> Before proceeding, please disable the 'Real-time protection' feature in `Windows Defender`. This feature may prevent `certutil` from downloading any files.\n\n<details>\n\n<summary>Resolving Script Download Issues on Windows 7</summary>\n\nDue 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:\n\nUse 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:\n\n- <https://raw.githubusercontent.com/bin456789/reinstall/main/reinstall.bat>\n\n- <https://www.cygwin.com/setup-x86.exe>\n\nTo use, run the downloaded `reinstall.bat`.\n\n</details>\n\nFor server outside China:\n\n```batch\ncertutil -urlcache -f -split https://raw.githubusercontent.com/bin456789/reinstall/main/reinstall.bat\n```\n\nFor server inside China:\n\n```batch\ncertutil -urlcache -f -split https://cnb.cool/bin456789/reinstall/-/git/raw/main/reinstall.bat\n```\n\n## Usage\n\n**All features** can be used on both Linux and Windows.\n\n- on Linux, run `bash reinstall.sh ...`\n- on Windows, first run `cmd`, then run `.\\reinstall.bat ...`\n  - If the link in the parameter contains special characters, it should be enclosed in `\"\"`, not `''`.\n\n### Feature 1: Install <img width=\"16\" height=\"16\" src=\"https://www.kernel.org/theme/images/logos/favicon.png\" /> Linux\n\n> [!CAUTION]\n>\n> This feature will erase **the entire hard disk** of the current system (including other partitions)!\n>\n> If the script was run by mistake, you can run `bash reinstall.sh reset` before rebooting to cancel the reinstallation operation.\n\n- Username `root`. The script prompts for a password. If left blank, a random one is generated.\n- When installing the latest version, the version number does not need to be specified.\n- Maximizes disk space usage: no boot partition (except for Fedora) and no swap partition.\n- Automatically selects different optimized kernels based on machine type, such as `Cloud` or `HWE` kernels.\n- When installing Red Hat, you must provide the `qcow2` image link obtained from <https://access.redhat.com/downloads/content/rhel>. You can also install `qcow2` of other RHEL-based OS, such as `Alibaba Cloud Linux` and `TencentOS Server`.\n- 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/`.\n\n```bash\nbash reinstall.sh anolis      7|8|23\n                  rocky       8|9|10\n                  oracle      8|9|10\n                  almalinux   8|9|10\n                  opencloudos 8|9|23\n                  centos      9|10\n                  fnos        1\n                  nixos       25.11\n                  fedora      42|43\n                  debian      9|10|11|12|13\n                  alpine      3.20|3.21|3.22|3.23\n                  opensuse    15.6|16.0|tumbleweed\n                  openeuler   20.03|22.03|24.03|25.09\n                  ubuntu      16.04|18.04|20.04|22.04|24.04|25.10 [--minimal]\n                  kali\n                  arch\n                  gentoo\n                  aosc\n                  redhat      --img=\"http://access.cdn.redhat.com/xxx.qcow2\"\n```\n\n#### Optional Parameters\n\n- `--password PASSWORD` Set the password\n- `--ssh-key KEY` Set up SSH login public key, [formatted as follows](#--ssh-key). When using public key, password is empty.\n- `--ssh-port PORT` Change the SSH port (for log observation during installation and for the new system)\n- `--web-port PORT` Change the Web port (for log observation during installation only)\n- `--frpc-config PATH` Add frpc for intranet tunneling. Parameter can be local filepath or HTTP URL of the configuration file.\n- `--hold 1` Reboot only into install environment, without running installer, only for SSH connect to test network connection.\n- `--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.\n\n> [!TIP]\n>\n> Can monitor the progress through various methods (SSH, HTTP 80 port, VNC from server provider, serial console).\n>\n> Even if errors occur during the installation process, SSH is available for manual recovery.\n>\n> If the target system is not Debian/Kali, run `/trans.sh alpine` can automatically recover to Alpine Linux.\n\n<details>\n\n<summary>Experimental Features</summary>\n\nInstall Debian using a cloud image\n\n- Suitable for machines with slower CPUs\n\n```bash\nbash reinstall.sh debian --ci\n```\n\nInstall CentOS, AlmaLinux, Rocky, Fedora using ISO\n\n- Only supports machines with more than 2G of memory and dynamic IP.\n- Password is `123@@@`, and the SSH port is `22`; modifying them using parameters is not supported.\n\n```bash\nbash reinstall.sh centos --installer\n```\n\nInstall Ubuntu using ISO\n\n- Only supports machines with more than 1G of memory and dynamic IP.\n- Password is `123@@@`, and the SSH port is `22`; modifying them using parameters is not supported.\n\n```bash\nbash reinstall.sh ubuntu --installer\n```\n\n</details>\n\n### Feature 2: DD RAW image to hard disk\n\n> [!CAUTION]\n>\n> This feature will erase **the entire hard disk** of the current system (including other partitions)!\n>\n> If the script was run by mistake, you can run `bash reinstall.sh reset` before rebooting to cancel the reinstallation operation.\n\n- Supports `raw` and fixed-size `vhd` image formats. Either uncompressed or compressed as `.gz`, `.xz`, `.zst`, `.tar`, `.tar.gz`, `.tar.xz`, `.tar.zst`.\n- 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.\n- When deploy a Linux image, will **NOT** modify any contents of the image.\n\n```bash\nbash reinstall.sh dd --img \"https://example.com/xxx.xz\"\n```\n\n#### Optional Parameters\n\n- `--allow-ping` Configure Windows Firewall to Allow Ping Responses (DD Windows only)\n- `--rdp-port PORT` Change RDP port (DD Windows only)\n- `--ssh-port PORT` Change SSH port (for log observation during installation)\n- `--web-port PORT` Change Web port (for log observation during installation)\n- `--frpc-config PATH` Add frpc for intranet tunneling (DD Windows only). Parameter can be local filepath or HTTP URL of the configuration file.\n- `--cloud-data PATH_OR_URL` Inject cloud-init NoCloud configuration into the DD'd Linux image (DD Linux only)\n- `--hold 1` Reboot only into install environment, without running installer, only for SSH connect to test network connection.\n- `--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.\n\n> [!TIP]\n>\n> `--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:\n>\n> ```\n> seed/\n> ├── user-data      # required\n> ├── meta-data      # optional\n> └── network-config # optional\n> ```\n>\n> ```bash\n> # Local directory\n> bash reinstall.sh dd --img \"https://example.com/xxx.xz\" --cloud-data /path/to/seed/\n> # HTTP directory\n> bash reinstall.sh dd --img \"https://example.com/xxx.xz\" --cloud-data \"https://example.com/seed/\"\n> ```\n\n> [!TIP]\n>\n> Can monitor the progress through various methods (SSH, HTTP 80 port, VNC from server provider, serial console).\n>\n> Even if errors occur during the installation process, SSH is available for manual recovery.\n>\n> Or Run `/trans.sh alpine` to automatically recover to Alpine Linux.\n\n### Feature 3: Reboot to <img width=\"16\" height=\"16\" src=\"https://www.alpinelinux.org/alpine-logo.ico\" /> Alpine Live OS\n\n- You can use SSH to backup/restore disk, manually perform DD operations, partition modifications, manual Alpine installation, and other operations.\n- Username `root`. The script prompts for a password. If left blank, a random one is generated.\n\n> [!TIP]\n>\n> Although the script being run is `reinstall`, this feature **does not** delete any data or perform an automatic reinstallation; manual user operation is required.\n>\n> If the user does not damage the original system during manual operation, rebooting will return to the original system.\n\n```bash\nbash reinstall.sh alpine --hold 1\n```\n\n#### Optional Parameters\n\n- `--password PASSWORD` Set password\n- `--ssh-port PORT` Change SSH port\n- `--ssh-key KEY` Set up SSH login public key, [formatted as follows](#--ssh-key). When using public key, password is empty.\n- `--frpc-config PATH` Add frpc for intranet tunneling. Parameter can be local filepath or HTTP URL of the configuration file.\n\n### Feature 4: Reboot to <img width=\"16\" height=\"16\" src=\"https://netboot.xyz/img/favicon.ico\" /> netboot.xyz\n\n- 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.\n\n> [!TIP]\n>\n> Although the script being run is `reinstall`, this feature **does not** delete any data or perform an automatic reinstallation; manual user operation is required.\n>\n> If the user does not damage the original system during manual operation, rebooting will return to the original system.\n\n```bash\nbash reinstall.sh netboot.xyz\n```\n\n![netboot.xyz](https://netboot.xyz/images/netboot.xyz.gif)\n\n### Feature 5: Install <img width=\"16\" height=\"16\" src=\"https://blogs.windows.com/wp-content/uploads/prod/2022/09/cropped-Windows11IconTransparent512-32x32.png\" /> Windows ISO\n\n![Windows Installation](https://github.com/bin456789/reinstall/assets/7548515/07c1aea2-1ce3-4967-904f-aaf9d6eec3f7)\n\n> [!CAUTION]\n>\n> This feature will erase **the entire hard disk** of the current system (including other partitions)!\n>\n> If the script was run by mistake, you can run `bash reinstall.sh reset` before rebooting to cancel the reinstallation operation.\n\n- Username `administrator`. The script prompts for a password. If left blank, a random one is generated.\n- If remote login fails, try using the username `.\\administrator`.\n- The machine with a static IP will automatically configure the IP. It may take a few minutes to take effect on the first boot.\n- Supports ISO images in any language.\n- Supports bypassing Windows 11 hardware requirements.\n\n#### Supported Systems\n\n- Windows (Vista ~ 11)\n- Windows Server (2008 ~ 2025)\n  - Windows Server Essentials\n  - Windows Server (Semi) Annual Channel\n  - Hyper-V Server\n  - Azure Local (Azure Stack HCI)\n\n#### Method 1: Let the Script Automatically Search for ISO\n\n- The script will search for ISOs from <https://massgrave.dev/genuine-installation-media>, a site that collects official ISOs.\n- Only supports ISOs searching for Windows 10, 11, Server 2019, 2022, 2025.\n\n```bash\nbash reinstall.sh windows \\\n     --image-name \"Windows 11 Enterprise LTSC 2024\" \\\n     --lang zh-cn\n```\n\n<details>\n<summary>Supported languages</summary>\n\n```text\nar-sa\nbg-bg\ncs-cz\nda-dk\nde-de\nel-gr\nen-gb\nen-us\nes-es\nes-mx\net-ee\nfi-fi\nfr-ca\nfr-fr\nhe-il\nhr-hr\nhu-hu\nit-it\nja-jp\nko-kr\nlt-lt\nlv-lv\nnb-no\nnl-nl\npl-pl\npt-pt\npt-br\nro-ro\nru-ru\nsk-sk\nsl-si\nsr-latn-rs\nsv-se\nth-th\ntr-tr\nuk-ua\nzh-cn\nzh-hk\nzh-tw\n```\n\n</details>\n\n#### Method 2: Specify the ISO link manually\n\n- 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.\n\n```bash\nbash reinstall.sh windows \\\n     --image-name \"Windows 11 Enterprise LTSC 2024 Evaluation\" \\\n     --iso \"https://go.microsoft.com/fwlink/?linkid=2289029\"\n```\n\nor Magnet Link\n\n```bash\nbash reinstall.sh windows \\\n     --image-name \"Windows 11 Enterprise LTSC 2024\" \\\n     --iso \"magnet:?xt=urn:btih:7352bd2db48c3381dffa783763dc75aa4a6f1cff\"\n```\n\n<details>\n\n<summary>The following website provides ISO links.</summary>\n\n- General\n  - <https://msdl.gravesoft.dev>\n  - <https://massgrave.dev/genuine-installation-media>\n  - <https://next.itellyou.cn>\n  - <https://www.xitongku.com>\n  - <https://www.microsoft.com/software-download/windows10> (Need to open it with a non-Windows User-Agent)\n  - <https://www.microsoft.com/software-download/windows11>\n  - <https://www.microsoft.com/software-download/windows11arm64>\n- Evaluation\n  - <https://www.microsoft.com/evalcenter/download-windows-11-enterprise>\n  - <https://www.microsoft.com/evalcenter/download-windows-11-iot-enterprise-ltsc-eval>\n  - <https://www.microsoft.com/evalcenter/download-windows-server-2012-r2>\n  - <https://www.microsoft.com/evalcenter/download-windows-server-2016>\n  - <https://www.microsoft.com/evalcenter/download-windows-server-2019>\n  - <https://www.microsoft.com/evalcenter/download-windows-server-2022>\n  - <https://www.microsoft.com/evalcenter/download-windows-server-2025>\n- Insider Preview\n  - <https://www.microsoft.com/en-us/software-download/windowsinsiderpreviewiso>\n  - <https://www.microsoft.com/en-us/software-download/windowsinsiderpreviewserver>\n\n</details>\n\n#### Optional Parameters\n\n- `--password PASSWORD` Set Password\n- `--allow-ping` Configure Windows Firewall to Allow Ping Responses\n- `--rdp-port PORT` Change RDP port\n- `--ssh-port PORT` Change SSH port (for log observation during installation only)\n- `--web-port PORT` Change Web port (for log observation during installation only)\n- `--add-driver INF_OR_DIR` Add additional driver, specifying .inf path, or the folder contains .inf file.\n  - The driver must be downloaded to current system first.\n  - This parameter can be set multiple times to add different driver.\n- `--frpc-config PATH` Add frpc for intranet tunneling. Parameter can be local filepath or HTTP URL of the configuration file.\n- `--hold 1` Reboot only into install environment, without running installer, only for SSH connect to test network connection.\n- `--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`.\n\n#### The following drivers will automatic download and install as needed, without the need for manual addition\n\n- VirtIO ([Community][virtio-virtio], [Alibaba Cloud][virtio-aliyun], [Tencent Cloud][virtio-qcloud], [GCP][virtio-gcp])\n- XEN ([~~Community~~][xen-xen] (unsigned), [Citrix][xen-citrix], [AWS][xen-aws])\n- AWS ([ENA Network Adapter][aws-ena], [NVME Storage Controller][aws-nvme])\n- GCP ([gVNIC Network Adapter][gcp-gvnic], [GGA Display Adapter][gcp-gga])\n- Azure ([MANA Network Adapter][azure-mana])\n- 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])\n\n[virtio-virtio]: https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/\n[virtio-aliyun]: https://www.alibabacloud.com/help/ecs/user-guide/install-the-virtio-driver-1\n[virtio-qcloud]: https://cloud.tencent.com/document/product/213/17815#b84b2032-752c-43c4-a509-73530b8f82ff\n[virtio-gcp]: https://console.cloud.google.com/storage/browser/gce-windows-drivers-public\n[xen-xen]: https://xenproject.org/resources/downloads/\n[xen-aws]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/xen-drivers-overview.html\n[xen-citrix]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Upgrading_PV_drivers.html#win2008-citrix-upgrade\n[aws-ena]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ena-driver-releases-windows.html\n[aws-nvme]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/nvme-driver-version-history.html\n[gcp-gvnic]: https://cloud.google.com/compute/docs/networking/using-gvnic\n[gcp-gga]: https://cloud.google.com/compute/docs/instances/enable-instance-virtual-display\n[azure-mana]: https://learn.microsoft.com/azure/virtual-network/accelerated-networking-mana-windows\n[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\n[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\n[intel-nic-7]: https://www.intel.com/content/www/us/en/download/15590/intel-network-adapter-driver-for-windows-7-final-release.html\n[intel-nic-8.1]: https://www.intel.com/content/www/us/en/download/17479/intel-network-adapter-driver-for-windows-8-1.html\n[intel-nic-10]: https://www.intel.com/content/www/us/en/download/18293/intel-network-adapter-driver-for-windows-10.html\n[intel-nic-11]: https://www.intel.com/content/www/us/en/download/727998/intel-network-adapter-driver-for-microsoft-windows-11.html\n[intel-nic-2012]: https://www.intel.com/content/www/us/en/download/16789/intel-network-adapter-driver-for-windows-server-2012.html\n[intel-nic-2012-r2]: https://www.intel.com/content/www/us/en/download/17480/intel-network-adapter-driver-for-windows-server-2012-r2.html\n[intel-nic-2016]: https://www.intel.com/content/www/us/en/download/18737/intel-network-adapter-driver-for-windows-server-2016.html\n[intel-nic-2019]: https://www.intel.com/content/www/us/en/download/19372/intel-network-adapter-driver-for-windows-server-2019.html\n[intel-nic-2022]: https://www.intel.com/content/www/us/en/download/706171/intel-network-adapter-driver-for-windows-server-2022.html\n[intel-nic-2025]: https://www.intel.com/content/www/us/en/download/838943/intel-network-adapter-driver-for-windows-server-2025.html\n\n#### How to Specify the Image Name `--image-name`\n\nAn 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.\n\nYou can use tools like DISM, DISM++, or Wimlib to query the image names included in the ISO.\n\nCommonly used image names include:\n\n```text\nWindows 7 Ultimate\nWindows 11 Pro\nWindows 11 Enterprise LTSC 2024\nWindows Server 2025 SERVERDATACENTER\n```\n\n#### How to Use [DISM++](https://github.com/Chuyu-Team/Dism-Multi-language/releases) to Query the Image Names Included in the ISO\n\nOpen 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.\n\n![image-name](https://github.com/bin456789/reinstall/assets/7548515/5aae0a9b-61e2-4f66-bb98-d470a6beaac2)\n\n> [!WARNING]\n> Vista (Server 2008) and 32-bit systems may lack drivers.\n\n> [!WARNING]\n>\n> For Windows 7 (Server 2008 R2) installation:\n>\n> 1. EFI-boot machines must enable CSM.\n>\n> 2. On Hyper-V (Azure), select Generation 1 VM. <https://learn.microsoft.com/windows-server/virtualization/hyper-v/plan/should-i-create-a-generation-1-or-2-virtual-machine-in-hyper-v>\n\n> [!WARNING]\n>\n> 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.\n>\n> The solution is to update the system patches or manually install the `VCLibs` library <https://www.google.com/search?q=ltsc+wsappx>.\n\n> [!WARNING]\n>\n> 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:\n>\n> 1. Add the `--force-boot-mode bios` parameter. The script will install Windows in `BIOS boot + MBR partition table` mode.\n>\n>    (Optional) After installation, you can convert it to `EFI boot + GPT partition table` using the command `MBR2GPT /convert /allowFullOS`.\n>\n> 2. Create a custom RAW image and install it via DD.\n\n#### Considerations for Installing Windows on ARM\n\nMost ARM machines support installing latest Windows 11.\n\nDuring 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.\n\n| Compatibility | Cloud Provider | Instance Type           | Issues                                                                              |\n| ------------- | -------------- | ----------------------- | ----------------------------------------------------------------------------------- |\n| ✔️            | Azure          | B2pts_v2                |                                                                                     |\n| ✔️            | AWS            | T4g                     |                                                                                     |\n| ✔️            | Scaleway       | COPARM1                 |                                                                                     |\n| ✔️            | Gcore          |                         |                                                                                     |\n| ❔            | Alibaba Cloud  | g6r, c6r, g8y, c8y, r8y | Might hanging at the boot logo during restart; forced reboot will resolve it.       |\n| ❔            | Oracle Cloud   | A1.Flex                 | Installation success is not guaranteed; newer instances are more likely to succeed. |\n| ❌            | Google Cloud   | t2a                     | Missing network card drivers                                                        |\n\n### Cancel the reinstallation\n\n- If the script was run by mistake, you can run this command to cancel the reinstallation operation.\n- Must be run before rebooting.\n\n```bash\nbash reinstall.sh reset\n```\n\n## Parameter Format\n\n### --ssh-key\n\n- `--ssh-key \"ssh-rsa ...\"`\n- `--ssh-key \"ssh-ed25519 ...\"`\n- `--ssh-key \"ecdsa-sha2-nistp256/384/521 ...\"`\n- `--ssh-key http://path/to/public_key`\n- `--ssh-key github:your_username`\n- `--ssh-key gitlab:your_username`\n- `--ssh-key /path/to/public_key`\n- `--ssh-key C:\\path\\to\\public_key`\n\n## How to Use an Old Version\n\nAccording to the Law of Bug Conservation, fixing old bugs often introduces new ones.\n\nIf a new bug occurs, try using an older version to see if it works.\n\nGo to <https://github.com/bin456789/reinstall/commits/main> and find the old version’s `commit_id` on the right side.\n\n```bash\ncommit_id=xxxxxxx\ncurl -O https://raw.githubusercontent.com/bin456789/reinstall/$commit_id/reinstall.sh || wget -O ${_##*/} $_\nsed -i \"/^confhome.*main$/s/main/$commit_id/\" reinstall.sh\nbash reinstall.sh ...\n```\n\n## How to Modify the Script for Your Own\n\n1. Fork this repository.\n2. Modify the `confhome` and `confhome_cn` at the beginning of `reinstall.sh` and `reinstall.bat`.\n3. Make changes to the other code.\n\n## Thanks\n\nThanks to the following businesses for providing free servers.\n\n[![Oracle Cloud](https://github.com/bin456789/reinstall/assets/7548515/8b430ed4-8344-4f96-b4da-c2bda031cc90)](https://www.oracle.com/cloud/)\n[![DartNode](https://github.com/bin456789/reinstall/assets/7548515/435d6740-bcdd-4f3a-a196-2f60ae397f17)](https://dartnode.com/)\n"
  },
  {
    "path": "README.md",
    "content": "<!-- markdownlint-disable MD028 MD033 MD045 -->\n\n# reinstall\n\n[![Codacy](https://img.shields.io/codacy/grade/dc679a17751448628fe6d8ac35e26eed?logo=Codacy&label=Codacy&style=flat-square)](https://app.codacy.com/gh/bin456789/reinstall/dashboard)\n[![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)\n[![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)\n<!-- [![Lines of Code](https://tokei.rs/b1/github/bin456789/reinstall?category=code&label=Lines%20of%20Code&style=flat-square)](https://github.com/XAMPPRocky/tokei_rs) -->\n\n一键 VPS 系统重装脚本 [English](README.en.md)\n\n## 介绍\n\n- 一键重装到 Linux，支持 19 种常见发行版\n- 一键重装到 Windows，使用官方原版 ISO 而非自制镜像，脚本支持自动查找 ISO 链接、自动安装 `VirtIO` 等公有云驱动\n- 支持任意方向重装，即 `Linux to Linux`、`Linux to Windows`、`Windows to Windows`、`Windows to Linux`\n- 自动设置 IP，智能设置动静态，支持 `/32`、`/128`、`网关不在子网范围内`、`纯 IPv6`、`IPv4/IPv6 在不同的网卡`\n- 专门适配低配小鸡，比官方 netboot 需要更少的内存\n- 全程用分区表 ID 识别硬盘，确保不会写错硬盘\n- 支持 BIOS、EFI 引导，支持 ARM 服务器\n- 不含自制包，所有资源均实时从镜像源获得\n\n如果帮到你，可以请我喝奶茶。\n[![Donate](https://img.shields.io/badge/Donate-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#EA4AAA)](https://github.com/sponsors/bin456789)\n\n[![Sponsors](https://raw.githubusercontent.com/bin456789/sponsors/refs/heads/master/sponsors.svg)](https://github.com/sponsors/bin456789)\n\n### 反馈\n\n[![GitHub Issues](https://img.shields.io/badge/GitHub-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/bin456789/reinstall/issues)\n[![Telegram Group](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/reinstall_os)\n\n## 快速开始\n\n- [下载](#下载当前系统是--linux)\n- [功能 1. 一键重装到 Linux](#功能-1-安装--linux)\n- [功能 2. 一键 DD Raw 镜像到硬盘](#功能-2-dd-raw-镜像到硬盘)\n- [功能 3. 一键引导到 Alpine Live OS 内存系统](#功能-3-重启到--alpine-live-os内存系统)\n- [功能 4. 一键引导到 netboot.xyz](#功能-4-重启到--netbootxyz)\n- [功能 5. 一键重装到 Windows](#功能-5-安装--windows-iso)\n- [取消重装](#取消重装)\n\n## 系统要求\n\n原系统可以是表格中的任意系统\n\n目标系统的配置要求如下：\n\n| 系统                                                                                                                                                                                                                                                                                                                                                                   | 版本                                  | 内存      | 硬盘         |\n| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | --------- | ------------ |\n| <img width=\"16\" height=\"16\" src=\"https://www.alpinelinux.org/alpine-logo.ico\" /> Alpine                                                                                                                                                                                                                                                                                | 3.20, 3.21, 3.22, 3.23                | 256 MB    | 1 GB         |\n| <img width=\"16\" height=\"16\" src=\"https://www.debian.org/favicon.ico\" /> Debian                                                                                                                                                                                                                                                                                         | 9, 10, 11, 12, 13                     | 256 MB    | 1 ~ 1.5 GB ^ |\n| <img width=\"16\" height=\"16\" src=\"https://github.com/bin456789/reinstall/assets/7548515/f74b3d5b-085f-4df3-bcc9-8a9bd80bb16d\" /> Kali                                                                                                                                                                                                                                   | 滚动                                  | 256 MB    | 1 ~ 1.5 GB ^ |\n| <img width=\"16\" height=\"16\" src=\"https://documentation.ubuntu.com/server/_static/favicon.png\" /> Ubuntu                                                                                                                                                                                                                                                                | 16.04 LTS - 24.04 LTS, 25.10          | 512 MB \\* | 2 GB         |\n| <img width=\"16\" height=\"16\" src=\"https://img.alicdn.com/imgextra/i1/O1CN01oJnJZg1yK4RzI4Rx2_!!6000000006559-2-tps-118-118.png\" /> Anolis                                                                                                                                                                                                                               | 7, 8, 23                              | 512 MB \\* | 5 GB         |\n| <img width=\"16\" height=\"16\" src=\"https://www.redhat.com/favicon.ico\" /> RHEL &nbsp;<img width=\"16\" height=\"16\" src=\"https://almalinux.org/fav/favicon.ico\" /> AlmaLinux &nbsp;<img width=\"16\" height=\"16\" src=\"https://rockylinux.org/favicon.png\" /> Rocky &nbsp;<img width=\"16\" height=\"16\" src=\"https://www.oracle.com/asset/web/favicons/favicon-32.png\" /> Oracle | 8, 9, 10                              | 512 MB \\* | 5 GB         |\n| <img width=\"16\" height=\"16\" src=\"https://opencloudos.org/qq.ico\" /> OpenCloudOS                                                                                                                                                                                                                                                                                        | 8, 9, Stream 23                       | 512 MB \\* | 5 GB         |\n| <img width=\"16\" height=\"16\" src=\"https://www.centos.org/assets/icons/favicon.svg\" /> CentOS Stream                                                                                                                                                                                                                                                                     | 9, 10                                 | 512 MB \\* | 5 GB         |\n| <img width=\"16\" height=\"16\" src=\"https://fedoraproject.org/favicon.ico\" /> Fedora                                                                                                                                                                                                                                                                                      | 42, 43                                | 512 MB \\* | 5 GB         |\n| <img width=\"16\" height=\"16\" src=\"https://www.openeuler.org/favicon.ico\" /> openEuler                                                                                                                                                                                                                                                                                   | 20.03 LTS - 24.03 LTS, 25.09          | 512 MB \\* | 5 GB         |\n| <img width=\"16\" height=\"16\" src=\"https://static.opensuse.org/favicon.ico\" /> openSUSE                                                                                                                                                                                                                                                                                  | Leap 15.6, 16.0, Tumbleweed (滚动)    | 512 MB \\* | 5 GB         |\n| <img width=\"16\" height=\"16\" src=\"https://nixos.org/favicon.svg\" /> NixOS                                                                                                                                                                                                                                                                                               | 25.11                                 | 512 MB    | 5 GB         |\n| <img width=\"16\" height=\"16\" src=\"https://archlinux.org/static/favicon.png\" /> Arch                                                                                                                                                                                                                                                                                     | 滚动                                  | 512 MB    | 5 GB         |\n| <img width=\"16\" height=\"16\" src=\"https://www.gentoo.org/assets/img/logo/gentoo-g.png\" /> Gentoo                                                                                                                                                                                                                                                                        | 滚动                                  | 512 MB    | 5 GB         |\n| <img width=\"16\" height=\"16\" src=\"https://aosc.io/distros/aosc-os.svg\" /> 安同 OS                                                                                                                                                                                                                                                                                       | 滚动                                  | 512 MB    | 5 GB         |\n| <img width=\"16\" height=\"16\" src=\"https://www.fnnas.com/favicon.ico\" /> 飞牛 fnOS                                                                                                                                                                                                                                                                                       | 1                                     | 512 MB    | 8 GB         |\n| <img width=\"16\" height=\"16\" src=\"https://blogs.windows.com/wp-content/uploads/prod/2022/09/cropped-Windows11IconTransparent512-32x32.png\" /> Windows (DD)                                                                                                                                                                                                              | 任何                                  | 512 MB    | 取决于镜像   |\n| <img width=\"16\" height=\"16\" src=\"https://blogs.windows.com/wp-content/uploads/prod/2022/09/cropped-Windows11IconTransparent512-32x32.png\" /> Windows (ISO)                                                                                                                                                                                                             | Vista, 7, 8.x (Server 2008 - 2012 R2) | 512 MB    | 25 GB        |\n| <img width=\"16\" height=\"16\" src=\"https://blogs.windows.com/wp-content/uploads/prod/2022/09/cropped-Windows11IconTransparent512-32x32.png\" /> Windows (ISO)                                                                                                                                                                                                             | 10, 11 (Server 2016 - 2025)           | 1 GB      | 25 GB        |\n\n\\* 表示使用云镜像安装，非传统网络安装\n\n^ 表示需要 256 MB 内存 + 1.5 GB 硬盘，或 512 MB 内存 + 1 GB 硬盘\n\n> [!WARNING]\n>\n> 本脚本理论上支持独服和 PC\n>\n> 但如果能使用 IPMI 或 U 盘，则不建议使用本脚本\n\n> [!WARNING]\n>\n> ❌ 本脚本不支持 OpenVZ、LXC 虚拟机\n>\n> 请改用 <https://github.com/LloydAsp/OsMutation>\n\n## 下载（当前系统是 <img width=\"20\" height=\"20\" src=\"https://www.kernel.org/theme/images/logos/favicon.png\" /> Linux）\n\n国外服务器：\n\n```bash\ncurl -O https://raw.githubusercontent.com/bin456789/reinstall/main/reinstall.sh || wget -O ${_##*/} $_\n```\n\n国内服务器：\n\n```bash\ncurl -O https://cnb.cool/bin456789/reinstall/-/git/raw/main/reinstall.sh || wget -O ${_##*/} $_\n```\n\n## 下载（当前系统是 <img width=\"20\" height=\"20\" src=\"https://blogs.windows.com/wp-content/uploads/prod/2022/09/cropped-Windows11IconTransparent512-32x32.png\" /> Windows）\n\n> [!IMPORTANT]\n> 请先关闭 `Windows Defender` 的 `实时保护` 功能。该功能会阻止 `certutil` 下载任何文件。\n\n<details>\n\n<summary>解决 Windows 7 下无法下载脚本</summary>\n\n由于不支持 TLS 1.2、SHA-256、根证书没有更新等原因，Vista，7 和 Server 2008 (R2) 可能无法自动下载脚本，因此需要手动下载，具体操作如下：\n\n用 IE 下载 (先在 IE 高级设置里启用 TLS 1.2)，或者通过远程桌面，将这两个文件保存到同一个目录\n\n- <https://raw.githubusercontent.com/bin456789/reinstall/main/reinstall.bat>\n\n- <https://www.cygwin.com/setup-x86.exe>\n\n使用时运行下载的 `reinstall.bat`\n\n</details>\n\n国外服务器：\n\n```batch\ncertutil -urlcache -f -split https://raw.githubusercontent.com/bin456789/reinstall/main/reinstall.bat\n```\n\n国内服务器：\n\n```batch\ncertutil -urlcache -f -split https://cnb.cool/bin456789/reinstall/-/git/raw/main/reinstall.bat\n```\n\n## 使用\n\n**所有功能** 都可在 Linux / Windows 下运行\n\n- Linux 下运行 `bash reinstall.sh ...`\n- Windows 下先运行 `cmd`，再运行 `reinstall.bat ...`\n  - 如果参数中的链接包含特殊字符，要用 `\"\"` 将链接包裹起来，不能用 `''`\n\n### 功能 1: 安装 <img width=\"16\" height=\"16\" src=\"https://www.kernel.org/theme/images/logos/favicon.png\" /> Linux\n\n> [!CAUTION]\n>\n> 此功能会清除当前系统**整个硬盘**的全部数据（包含其它分区）！\n>\n> 如果不小心运行了脚本，可以在重启前运行 `bash reinstall.sh reset` 取消重装\n\n- 用户名为 `root`，脚本会提示输入密码，不输入则使用随机密码\n- 安装最新版可不输入版本号\n- 最大化利用磁盘空间：不含 boot 分区（Fedora 例外），不含 swap 分区\n- 自动根据机器类型选择不同的优化内核，例如 `Cloud`、`HWE` 内核\n- 安装 Red Hat 时需填写 <https://access.redhat.com/downloads/content/rhel> 得到的 `qcow2` 镜像链接，也可以安装其它类 RHEL 系统的 `qcow2`，例如 `Alibaba Cloud Linux` 和 `TencentOS Server`\n- 重装后如需修改 SSH 端口或者改成密钥登录，注意还要修改 `/etc/ssh/sshd_config.d/` 里面的文件\n\n```bash\nbash reinstall.sh anolis      7|8|23\n                  rocky       8|9|10\n                  oracle      8|9|10\n                  almalinux   8|9|10\n                  opencloudos 8|9|23\n                  centos      9|10\n                  fnos        1\n                  nixos       25.11\n                  fedora      42|43\n                  debian      9|10|11|12|13\n                  alpine      3.20|3.21|3.22|3.23\n                  opensuse    15.6|16.0|tumbleweed\n                  openeuler   20.03|22.03|24.03|25.09\n                  ubuntu      16.04|18.04|20.04|22.04|24.04|25.10 [--minimal]\n                  kali\n                  arch\n                  gentoo\n                  aosc\n                  redhat      --img=\"http://access.cdn.redhat.com/xxx.qcow2\"\n```\n\n#### 可选参数\n\n- `--password PASSWORD` 设置密码\n- `--ssh-key KEY` 设置 SSH 登录公钥，[格式如下](#--ssh-key)。当使用公钥时，密码为空\n- `--ssh-port PORT` 修改 SSH 端口（安装期间观察日志用，也作用于新系统）\n- `--web-port PORT` 修改 Web 端口（安装期间观察日志用）\n- `--frpc-config PATH` 添加 frpc 内网穿透，参数填配置文件的本地路径或 HTTP 链接\n- `--hold 1` 仅重启到安装环境，不运行安装，用于 SSH 登录验证网络连通性\n- `--hold 2` 安装结束后不重启，用于 SSH 登录修改系统内容，Debian/Kali 会挂载在 `/target`，其它系统会挂载在 `/os`\n\n> [!TIP]\n>\n> 可通过多种方式（SSH、HTTP 80 端口、商家后台 VNC、串行控制台）查看安装进度。\n>\n> 即使安装过程出错，也能连接 SSH 手动救砖。\n>\n> 目标系统非 Debian/Kali 时，可以运行 `/trans.sh alpine` 自动救砖成 Alpine 系统。\n\n<details>\n\n<summary>实验性功能</summary>\n\n云镜像安装 Debian\n\n- 适合于 CPU 较慢的机器\n\n```bash\nbash reinstall.sh debian --ci\n```\n\nISO 安装 CentOS, AlmaLinux, Rocky, Fedora\n\n- 仅支持内存大于 2G 且为动态 IP 的机器\n- 密码 `123@@@`，SSH 端口 `22`，不支持用参数修改\n\n```bash\nbash reinstall.sh centos --installer\n```\n\nISO 安装 Ubuntu\n\n- 仅支持内存大于 1G 且为动态 IP 的机器\n- 密码 `123@@@`，SSH 端口 `22`，不支持用参数修改\n\n```bash\nbash reinstall.sh ubuntu --installer\n```\n\n</details>\n\n### 功能 2: DD RAW 镜像到硬盘\n\n> [!CAUTION]\n>\n> 此功能会清除当前系统**整个硬盘**的全部数据（包含其它分区）！\n>\n> 如果不小心运行了脚本，可以在重启前运行 `bash reinstall.sh reset` 取消重装\n\n- 支持 `raw` 和固定大小的 `vhd` 镜像。未压缩或者压缩成 `.gz` `.xz` `.zst` `.tar` `.tar.gz` `.tar.xz` `.tar.zst`\n- DD Windows 镜像时，会自动扩展系统盘，静态 IP 的机器会配置好 IP，可能首次开机几分钟后才生效\n- DD Linux 镜像时，**不会**修改镜像的任何内容\n\n```bash\nbash reinstall.sh dd --img \"https://example.com/xxx.xz\"\n```\n\n#### 可选参数\n\n- `--allow-ping` 设置 Windows 防火墙允许被 Ping (仅限 DD Windows)\n- `--rdp-port PORT` 修改 RDP 端口 (仅限 DD Windows)\n- `--ssh-port PORT` 修改 SSH 端口（安装期间观察日志用）\n- `--web-port PORT` 修改 Web 端口（安装期间观察日志用）\n- `--frpc-config PATH` 添加 frpc 内网穿透（仅限 DD Windows），参数填配置文件的本地路径或 HTTP 链接\n- `--cloud-data PATH_OR_URL` 为 DD Linux 镜像注入 cloud-init NoCloud 配置（仅限 DD Linux）\n- `--hold 1` 仅重启到安装环境，不运行安装，用于 SSH 登录验证网络连通性\n- `--hold 2` DD 结束后不重启，用于 SSH 登录修改系统内容，Windows 系统会挂载在 `/os`，Linux 系统**不会**自动挂载\n\n> [!TIP]\n>\n> `--cloud-data` 参数为本地目录或 HTTP 基础 URL，目录须包含 `user-data` 文件，`meta-data`、`network-config` 可选：\n>\n> ```\n> seed/\n> ├── user-data      # 必须\n> ├── meta-data      # 可选\n> └── network-config # 可选\n> ```\n>\n> ```bash\n> # 使用本地目录\n> bash reinstall.sh dd --img \"https://example.com/xxx.xz\" --cloud-data /path/to/seed/\n> # 使用 HTTP 目录\n> bash reinstall.sh dd --img \"https://example.com/xxx.xz\" --cloud-data \"https://example.com/seed/\"\n> ```\n\n> [!TIP]\n>\n> 可通过多种方式（SSH、HTTP 80 端口、商家后台 VNC、串行控制台）查看安装进度。\n>\n> 即使安装过程出错，也能连接 SSH 手动救砖\n>\n> 也可以运行 `/trans.sh alpine` 自动救砖成 Alpine 系统。\n\n### 功能 3: 重启到 <img width=\"16\" height=\"16\" src=\"https://www.alpinelinux.org/alpine-logo.ico\" /> Alpine Live OS（内存系统）\n\n- 可用 ssh 连接，进行备份/恢复硬盘、手动 DD、修改分区、手动安装 Alpine 等操作\n- 用户名为 `root`，脚本会提示输入密码，不输入则使用随机密码\n\n> [!TIP]\n>\n> 虽然运行的脚本叫 `reinstall`，但是此功能**不会**删除任何数据和进行自动重装，而是要用户手动操作\n>\n> 如果用户手动操作没有破坏原系统，再次重启将回到原系统\n\n```bash\nbash reinstall.sh alpine --hold 1\n```\n\n#### 可选参数\n\n- `--password PASSWORD` 设置密码\n- `--ssh-port PORT` 修改 SSH 端口\n- `--ssh-key KEY` 设置 SSH 登录公钥，[格式如下](#--ssh-key)。当使用公钥时，密码为空\n- `--frpc-config PATH` 添加 frpc 内网穿透，参数填配置文件的本地路径或 HTTP 链接\n\n### 功能 4: 重启到 <img width=\"16\" height=\"16\" src=\"https://netboot.xyz/img/favicon.ico\" /> netboot.xyz\n\n- 可使用商家后台 VNC 手动安装 [更多系统](https://github.com/netbootxyz/netboot.xyz?tab=readme-ov-file#what-operating-systems-are-currently-available-on-netbootxyz)\n\n> [!TIP]\n>\n> 虽然运行的脚本叫 `reinstall`，但是此功能**不会**删除任何数据和进行自动重装，而是要用户手动操作\n>\n> 如果用户手动操作没有破坏原系统，再次重启将回到原系统\n\n```bash\nbash reinstall.sh netboot.xyz\n```\n\n![netboot.xyz](https://netboot.xyz/images/netboot.xyz.gif)\n\n### 功能 5: 安装 <img width=\"16\" height=\"16\" src=\"https://blogs.windows.com/wp-content/uploads/prod/2022/09/cropped-Windows11IconTransparent512-32x32.png\" /> Windows ISO\n\n![Windows 安装界面](https://github.com/bin456789/reinstall/assets/7548515/07c1aea2-1ce3-4967-904f-aaf9d6eec3f7)\n\n> [!CAUTION]\n>\n> 此功能会清除当前系统**整个硬盘**的全部数据（包含其它分区）！\n>\n> 如果不小心运行了脚本，可以在重启前运行 `bash reinstall.sh reset` 取消重装\n\n- 用户名为 `administrator`，脚本会提示输入密码，不输入则使用随机密码\n- 如果远程登录失败，可以尝试使用用户名 `.\\administrator`\n- 静态机器会自动配置好 IP，可能首次开机几分钟后才生效\n- 支持任意语言的 ISO\n- 支持绕过 Windows 11 硬件限制\n\n#### 支持的系统\n\n- Windows (Vista ~ 11)\n- Windows Server (2008 ~ 2025)\n  - Windows Server Essentials\n  - Windows Server (Semi) Annual Channel\n  - Hyper-V Server\n  - Azure Local (Azure Stack HCI)\n\n#### 方法 1: 让脚本自动查找 ISO\n\n- 脚本会从 <https://massgrave.dev/genuine-installation-media> 查找 ISO，该网站专门提供官方 ISO 下载\n- 只支持查找 Windows 10, 11, Server 2019, 2022, 2025 的 ISO\n\n```bash\nbash reinstall.sh windows \\\n     --image-name \"Windows 11 Enterprise LTSC 2024\" \\\n     --lang zh-cn\n```\n\n<details>\n<summary>支持的语言</summary>\n\n```text\nar-sa\nbg-bg\ncs-cz\nda-dk\nde-de\nel-gr\nen-gb\nen-us\nes-es\nes-mx\net-ee\nfi-fi\nfr-ca\nfr-fr\nhe-il\nhr-hr\nhu-hu\nit-it\nja-jp\nko-kr\nlt-lt\nlv-lv\nnb-no\nnl-nl\npl-pl\npt-pt\npt-br\nro-ro\nru-ru\nsk-sk\nsl-si\nsr-latn-rs\nsv-se\nth-th\ntr-tr\nuk-ua\nzh-cn\nzh-hk\nzh-tw\n```\n\n</details>\n\n#### 方法 2: 自行指定 ISO 连接\n\n- 如果不知道 `--image-name`，可以随便填，在重启后连接 SSH，根据错误提示重新输入正确的值\n\n```bash\nbash reinstall.sh windows \\\n     --image-name \"Windows 11 Enterprise LTSC 2024 Evaluation\" \\\n     --iso \"https://go.microsoft.com/fwlink/?linkid=2289029\"\n```\n\n或者磁力链接\n\n```bash\nbash reinstall.sh windows \\\n     --image-name \"Windows 11 Enterprise LTSC 2024\" \\\n     --iso \"magnet:?xt=urn:btih:7352bd2db48c3381dffa783763dc75aa4a6f1cff\"\n```\n\n<details>\n\n<summary>以下网站可找到 ISO 链接</summary>\n\n- 正式版\n  - <https://msdl.gravesoft.dev>\n  - <https://massgrave.dev/genuine-installation-media>\n  - <https://next.itellyou.cn>\n  - <https://www.xitongku.com>\n  - <https://www.microsoft.com/software-download/windows10> (需用非 Windows User-Agent 打开)\n  - <https://www.microsoft.com/software-download/windows11>\n  - <https://www.microsoft.com/software-download/windows11arm64>\n- 评估版\n  - <https://www.microsoft.com/evalcenter/download-windows-11-enterprise>\n  - <https://www.microsoft.com/evalcenter/download-windows-11-iot-enterprise-ltsc-eval>\n  - <https://www.microsoft.com/evalcenter/download-windows-server-2012-r2>\n  - <https://www.microsoft.com/evalcenter/download-windows-server-2016>\n  - <https://www.microsoft.com/evalcenter/download-windows-server-2019>\n  - <https://www.microsoft.com/evalcenter/download-windows-server-2022>\n  - <https://www.microsoft.com/evalcenter/download-windows-server-2025>\n- Insider 预览版\n  - <https://www.microsoft.com/en-us/software-download/windowsinsiderpreviewiso>\n  - <https://www.microsoft.com/en-us/software-download/windowsinsiderpreviewserver>\n\n</details>\n\n#### 可选参数\n\n- `--password PASSWORD` 设置密码\n- `--allow-ping` 设置 Windows 防火墙允许被 Ping\n- `--rdp-port PORT` 更改 RDP 端口\n- `--ssh-port PORT` 修改 SSH 端口（仅安装期间观察日志用）\n- `--web-port PORT` 修改 Web 端口（仅安装期间观察日志用）\n- `--add-driver INF_OR_DIR` 添加额外驱动，填写 .inf 路径，或者 .inf 所在的文件夹\n  - 需先下载驱动到当前系统\n  - 可多次设置该参数以添加不同的驱动\n- `--frpc-config PATH` 添加 frpc 内网穿透，参数填配置文件的本地路径或 HTTP 链接\n- `--hold 1` 仅重启到安装环境，不运行安装，用于 SSH 登录验证网络连通性\n- `--hold 2` 用于在进入 Windows 官方安装程序之前，SSH 登录修改 `boot.wim`、`install.wim` 或者其它内容，硬盘挂载在 `/os`\n\n#### 以下驱动会自动按需下载安装，无需手动添加\n\n- VirtIO ([社区版][virtio-virtio], [阿里云][virtio-aliyun], [腾讯云][virtio-qcloud], [GCP][virtio-gcp])\n- XEN ([~~社区版~~][xen-xen] (未签名), [Citrix][xen-citrix], [AWS][xen-aws])\n- AWS ([ENA 网卡][aws-ena], [NVME 存储控制器][aws-nvme])\n- GCP ([gVNIC 网卡][gcp-gvnic], [GGA 显卡][gcp-gga])\n- Azure ([MANA 网卡][azure-mana])\n- 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])\n\n[virtio-virtio]: https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/\n[virtio-aliyun]: https://www.alibabacloud.com/help/ecs/user-guide/install-the-virtio-driver-1\n[virtio-qcloud]: https://cloud.tencent.com/document/product/213/17815#b84b2032-752c-43c4-a509-73530b8f82ff\n[virtio-gcp]: https://console.cloud.google.com/storage/browser/gce-windows-drivers-public\n[xen-xen]: https://xenproject.org/resources/downloads/\n[xen-aws]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/xen-drivers-overview.html\n[xen-citrix]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Upgrading_PV_drivers.html#win2008-citrix-upgrade\n[aws-ena]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ena-driver-releases-windows.html\n[aws-nvme]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/nvme-driver-version-history.html\n[gcp-gvnic]: https://cloud.google.com/compute/docs/networking/using-gvnic\n[gcp-gga]: https://cloud.google.com/compute/docs/instances/enable-instance-virtual-display\n[azure-mana]: https://learn.microsoft.com/azure/virtual-network/accelerated-networking-mana-windows\n[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\n[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\n[intel-nic-7]: https://www.intel.com/content/www/us/en/download/15590/intel-network-adapter-driver-for-windows-7-final-release.html\n[intel-nic-8.1]: https://www.intel.com/content/www/us/en/download/17479/intel-network-adapter-driver-for-windows-8-1.html\n[intel-nic-10]: https://www.intel.com/content/www/us/en/download/18293/intel-network-adapter-driver-for-windows-10.html\n[intel-nic-11]: https://www.intel.com/content/www/us/en/download/727998/intel-network-adapter-driver-for-microsoft-windows-11.html\n[intel-nic-2012]: https://www.intel.com/content/www/us/en/download/16789/intel-network-adapter-driver-for-windows-server-2012.html\n[intel-nic-2012-r2]: https://www.intel.com/content/www/us/en/download/17480/intel-network-adapter-driver-for-windows-server-2012-r2.html\n[intel-nic-2016]: https://www.intel.com/content/www/us/en/download/18737/intel-network-adapter-driver-for-windows-server-2016.html\n[intel-nic-2019]: https://www.intel.com/content/www/us/en/download/19372/intel-network-adapter-driver-for-windows-server-2019.html\n[intel-nic-2022]: https://www.intel.com/content/www/us/en/download/706171/intel-network-adapter-driver-for-windows-server-2022.html\n[intel-nic-2025]: https://www.intel.com/content/www/us/en/download/838943/intel-network-adapter-driver-for-windows-server-2025.html\n\n#### 如何填写映像名称 `--image-name`\n\n一个 ISO 通常包含多个系统版本，例如家庭版、专业版。因此需要用 `--image-name` 指定要安装的系统版本（映像名称），不区分大小写\n\n可以用 DISM、DISM++、Wimlib 等工具查询 ISO 包含的映像名称\n\n常用的映像名称有：\n\n```text\nWindows 7 Ultimate\nWindows 11 Pro\nWindows 11 Enterprise LTSC 2024\nWindows Server 2025 SERVERDATACENTER\n```\n\n#### 如何用 [DISM++](https://github.com/Chuyu-Team/Dism-Multi-language/releases) 查询 ISO 包含的映像名称\n\n打开文件菜单 > 打开映像文件，选择要安装的 iso，即可得到映像名称，所有映像名称都可以安装\n\n![image-name](https://github.com/bin456789/reinstall/assets/7548515/5aae0a9b-61e2-4f66-bb98-d470a6beaac2)\n\n> [!WARNING]\n> Vista (Server 2008) 和 32 位系统可能会缺少驱动\n\n> [!WARNING]\n>\n> 安装 Windows 7 (Server 2008 R2) 时\n>\n> 1. EFI 引导的机器要开启 CSM\n>\n> 2. Hyper-V (Azure) 需选择第 1 代虚拟机 <https://learn.microsoft.com/windows-server/virtualization/hyper-v/plan/should-i-create-a-generation-1-or-2-virtual-machine-in-hyper-v>\n\n> [!WARNING]\n>\n> Windows 10 LTSC 2021 中文版镜像 `zh-cn_windows_10_enterprise_ltsc_2021_x64_dvd_033b7312.iso` 的 `wsappx` 进程会长期占用 CPU\n>\n> 解决方法是更新系统补丁，或者手动安装 `VCLibs` 库 <https://www.google.com/search?q=ltsc+wsappx>\n\n> [!WARNING]\n>\n> 在 GCP 上安装 `2022年5月` 和之后发布的 Windows ISO，在引导 Windows 安装界面 (PE) 时会不断反复重启。解决方法如下，二选一\n>\n> 1. 添加 `--force-boot-mode bios` 参数，脚本将以 `BIOS 引导 + MBR 分区表` 方式安装 Windows\n>\n>    (可选) 安装完成后用 `MBR2GPT /convert /allowFullOS` 命令转为 `EFI 引导 + GPT 分区表`\n>\n> 2. 自制 RAW 镜像并通过 DD 安装\n\n#### ARM 安装 Windows 的注意事项\n\n大部分 ARM 机器都支持安装最新版 Windows 11\n\n安装过程可能会黑屏，串行控制台可能会显示 `ConvertPages: failed to find range`，均不影响正常安装\n\n| 兼容性 | 云服务商 | 实例类型                | 问题                                       |\n| ------ | -------- | ----------------------- | ------------------------------------------ |\n| ✔️     | Azure    | B2pts_v2                |                                            |\n| ✔️     | AWS      | T4g                     |                                            |\n| ✔️     | Scaleway | COPARM1                 |                                            |\n| ✔️     | Gcore    |                         |                                            |\n| ❔     | 阿里云   | g6r, c6r, g8y, c8y, r8y | 有几率重启时卡开机 Logo，强制重启即可      |\n| ❔     | 甲骨文云 | A1.Flex                 | 不一定能安装成功，越新创建的实例越容易成功 |\n| ❌     | 谷歌云   | t2a                     | 缺少网卡驱动                               |\n\n### 取消重装\n\n- 如果不小心运行了脚本，可以运行以下命令取消重装\n- 需要在重启前运行\n\n```bash\nbash reinstall.sh reset\n```\n\n## 参数格式\n\n### --ssh-key\n\n- `--ssh-key \"ssh-rsa ...\"`\n- `--ssh-key \"ssh-ed25519 ...\"`\n- `--ssh-key \"ecdsa-sha2-nistp256/384/521 ...\"`\n- `--ssh-key http://path/to/public_key`\n- `--ssh-key github:your_username`\n- `--ssh-key gitlab:your_username`\n- `--ssh-key /path/to/public_key`\n- `--ssh-key C:\\path\\to\\public_key`\n\n## 如何使用旧版本\n\n根据 Bug 守恒定律，修复旧 Bug 的同时会引入新的 Bug\n\n如果遇到新的 Bug，可以试下旧版本是否正常\n\n从 <https://github.com/bin456789/reinstall/commits/main> 右侧找到旧版本的 `commit_id`\n\n```bash\ncommit_id=xxxxxxx\ncurl -O https://raw.githubusercontent.com/bin456789/reinstall/$commit_id/reinstall.sh || wget -O ${_##*/} $_\nsed -i \"/^confhome.*main$/s/main/$commit_id/\" reinstall.sh\nbash reinstall.sh ...\n```\n\n## 如何修改脚本自用\n\n1. Fork 本仓库\n2. 修改 `reinstall.sh` 和 `reinstall.bat` 开头的 `confhome` 和 `confhome_cn`\n3. 修改其它代码\n\n## 感谢\n\n感谢以下商家提供白嫖机器\n\n[![Oracle Cloud](https://github.com/bin456789/reinstall/assets/7548515/8b430ed4-8344-4f96-b4da-c2bda031cc90)](https://www.oracle.com/cloud/)\n[![DartNode](https://github.com/bin456789/reinstall/assets/7548515/435d6740-bcdd-4f3a-a196-2f60ae397f17)](https://dartnode.com/)\n"
  },
  {
    "path": "cloud-init-fix-onlink.sh",
    "content": "#!/bin/bash\n# 修复 cloud-init 没有正确渲染 onlink 网关\n\nset -eE\nos_dir=$1\n\n# 该脚本也会在 alpine live 下调用\n# 防止在 alpine live 下运行 systemctl netplan 报错\nsystemctl() {\n    if systemd-detect-virt --chroot; then\n        return\n    fi\n    command systemctl \"$@\"\n}\n\nnetplan() {\n    if systemd-detect-virt --chroot; then\n        return\n    fi\n    command netplan \"$@\"\n}\n\ninsert_into_file() {\n    file=$1\n    location=$2\n    regex_to_find=$3\n\n    if [ \"$location\" = head ]; then\n        bak=$(mktemp)\n        cp \"$file\" \"$bak\"\n        cat - \"$bak\" >\"$file\"\n    else\n        line_num=$(grep -E -n \"$regex_to_find\" \"$file\" | cut -d: -f1)\n\n        found_count=$(echo \"$line_num\" | wc -l)\n        if [ ! \"$found_count\" -eq 1 ]; then\n            return 1\n        fi\n\n        case \"$location\" in\n        before) line_num=$((line_num - 1)) ;;\n        after) ;;\n        *) return 1 ;;\n        esac\n\n        sed -i \"${line_num}r /dev/stdin\" \"$file\"\n    fi\n}\n\nfix_netplan_conf() {\n    # 修改前\n    # gateway4: 1.1.1.1\n    # gateway6: ::1\n\n    # 修改后\n    # routes:\n    #   - to: 0.0.0.0/0\n    #     via: 1.1.1.1\n    #     on-link: true\n    # routes:\n    #   - to: ::/0\n    #     via: ::1\n    #     on-link: true\n    conf=$os_dir/etc/netplan/50-cloud-init.yaml\n    if ! [ -f \"$conf\" ]; then\n        return\n    fi\n\n    # 判断 bug 是否已经修复\n    if grep -q 'on-link:' \"$conf\"; then\n        return\n    fi\n\n    # 获取网关\n    gateways=$(grep 'gateway[4|6]:' \"$conf\" | awk '{print $2}')\n    if [ -z \"$gateways\" ]; then\n        return\n    fi\n\n    # 获取缩进\n    spaces=$(grep 'gateway[4|6]:' \"$conf\" | head -1 | grep -o '^[[:space:]]*')\n\n    {\n        # 网关头部\n        cat <<EOF\n${spaces}routes:\nEOF\n        # 网关条目\n        for gateway in $gateways; do\n            # debian 11 的 netplan 不支持 to: default\n            case $gateway in\n            *.*) to='0.0.0.0/0' ;;\n            *:*) to='::/0' ;;\n            esac\n\n            cat <<EOF\n${spaces}  - to: $to\n${spaces}    via: $gateway\n${spaces}    on-link: true\nEOF\n        done\n    } | insert_into_file \"$conf\" before 'match:'\n\n    # 删除原来的条目\n    sed -i '/gateway[4|6]:/d' \"$conf\"\n\n    # 重新应用配置\n    if command -v netplan && {\n        systemctl -q is-enabled systemd-networkd || systemctl -q is-enabled NetworkManager\n    }; then\n        netplan apply\n    fi\n}\n\nfix_networkd_conf() {\n    # 修改前 gentoo\n    # [Route]\n    # Gateway=1.1.1.1\n    # Gateway=2602::1\n\n    # 修改前 arch\n    # [Route]\n    # Gateway=1.1.1.1\n    #\n    # [Route]\n    # Gateway=2602::1\n\n    # 修改后\n    # [Route]\n    # Gateway=1.1.1.1\n    # GatewayOnLink=yes\n    #\n    # [Route]\n    # Gateway=2602::1\n    # GatewayOnLink=yes\n\n    if ! confs=$(ls \"$os_dir\"/etc/systemd/network/10-cloud-init-*.network 2>/dev/null); then\n        return\n    fi\n\n    for conf in $confs; do\n        # 判断 bug 是否已经修复\n        if grep -q '^GatewayOnLink=' \"$conf\"; then\n            return\n        fi\n\n        # 获取网关\n        gateways=$(grep '^Gateway=' \"$conf\" | cut -d= -f2)\n        if [ -z \"$gateways\" ]; then\n            return\n        fi\n\n        # 删除原来的条目\n        sed -i '/^\\[Route\\]/d; /^Gateway=/d; /^GatewayOnLink=/d' \"$conf\"\n\n        # 创建新条目\n        for gateway in $gateways; do\n            echo \"\n[Route]\nGateway=$gateway\nGatewayOnLink=yes\n\"\n        done >>\"$conf\"\n    done\n\n    # 重新应用配置\n    # networkctl reload 不起作用\n    if systemctl -q is-enabled systemd-networkd; then\n        systemctl restart systemd-networkd\n    fi\n}\n\nfix_wicked_conf() {\n    # https://github.com/openSUSE/wicked/wiki/FAQ#q-why-wicked-does-not-set-my-default-static-route\n\n    # 修改前\n    # default 1.1.1.1 - -\n    # default 2602::1 - -\n\n    # 修改后\n    # 1.1.1.1 - -\n    # 2602::1 - -\n    # default 1.1.1.1 - -\n    # default 2602::1 - -\n\n    if ! confs=$(ls \"$os_dir/etc/sysconfig/network/ifroute-\"* 2>/dev/null); then\n        return\n    fi\n\n    for conf in $confs; do\n        # 判断 bug 是否已经修复\n        if grep -v 'default' \"$conf\" | grep -q '-'; then\n            return\n        fi\n\n        # 获取网关\n        gateways=$(awk '$1==\"default\" {print $2}' \"$conf\")\n        if [ -z \"$gateways\" ]; then\n            return\n        fi\n\n        # 创建新条目\n        for gateway in $gateways; do\n            echo \"$gateway - -\"\n        done | insert_into_file \"$conf\" head\n    done\n\n    # 重新应用配置\n    if systemctl -q is-enabled wicked; then\n        systemctl restart wicked\n    fi\n}\n\n# ubuntu 18.04 cloud-init 版本 23.1.2，因此不用处理\n\n# debian 10/11 云镜像原本用 ifupdown + resolvconf，脚本改成用 netplan + networkd/resolved\n# debian 12 云镜像: netplan + networkd/resolved\n# 23.1.1 修复\nfix_netplan_conf\n\n# arch: networkd/resolved\n# gentoo: networkd/resolved\n# 24.2 修复\n# 只需对云镜像处理\n# 因为普通安装用的是 alpine 的 cloud-init，版本够新，不用处理\nfix_networkd_conf\n\n# opensuse 15.5: ifcfg + netconfig (dns) + wicked\nfix_wicked_conf\n"
  },
  {
    "path": "cloud-init.yaml",
    "content": "#cloud-config\ndatasource_list: [None]\ntimezone: Asia/Shanghai\ndisable_root: false\nssh_pwauth: true\nusers:\n  - name: root\n    lock_passwd: false\nchpasswd:\n  expire: false\n  # <= cloud-init 22.2.x 需要\n  list: |\n    root:@PASSWORD@\n  users:\n    - name: root\n      password: \"@PASSWORD@\"\n      type: hash\nruncmd:\n  # opensuse tumbleweed 镜像有 /etc/ssh/sshd_config.d/ 文件夹，没有 /etc/ssh/sshd_config，有/usr/etc/ssh/sshd_config\n  # opensuse tumbleweed cloud-init 直接创建并写入 /etc/ssh/sshd_config，造成默认配置丢失\n  # 下面这行删除 clout-init 创建的 sshd_config\n  - test $(wc -l </etc/ssh/sshd_config) -le 1 && cat /etc/ssh/sshd_config >>/etc/ssh/sshd_config.d/50-cloud-init.conf && rm -f /etc/ssh/sshd_config\n  - echo \"PermitRootLogin yes\" >/etc/ssh/sshd_config.d/01-permitrootlogin.conf 2>/dev/null || sed -Ei 's/^#?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config\n  - 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\n  # 已创建的 ssh 连接会沿用旧的配置（未开启密码登录），这时即使输入正确的密码，也会提示 Access Denied\n  # systemctl restart sshd 只会重启监听进程，不会关闭已创建的连接（子进程）\n  - pkill sshd || true\n  # daemon-reload 会刷新 /run/systemd/generator/ssh.socket.d/addresses.conf\n  - systemctl daemon-reload\n  - for s in ssh.socket ssh.service sshd.socket sshd.service; do systemctl is-enabled $s 2>/dev/null && systemctl restart $s && break; done\n  # 删除有密码的行\n  - sed -i -e '/^[[:space:]]*password:/d' -e '/[[:space:]]*root:/d' /etc/cloud/cloud.cfg.d/99_fallback.cfg\n  - touch /etc/cloud/cloud-init.disabled\n  # ubuntu 镜像运行 echo -e '\\nDone' ，-e 会被显示出来\n  # 加 true 因为有的 tty 不可写\n  - for tty in tty0 ttyS0 ttyAMA0; do [ -c /dev/$tty ] && printf '\\n%s\\n' 'reinstall done' >/dev/$tty || true; done\n"
  },
  {
    "path": "debian.cfg",
    "content": "#_preseed_V1\n# shellcheck disable=SC1091,SC2148\n# https://www.debian.org/releases/stable/amd64/apbs04.zh-cn.html\n# https://www.debian.org/releases/stable/example-preseed.txt\n# https://preseed.debian.net/debian-preseed/trixie/amd64-main-full.txt\n# 需要留意 kali initrd 自带的 /preseed.cfg\n\n  # 下面这行语句无效，因为本行后面有反斜杠，前面有空格（安装器认为不算注释）\\\nd-i debian-installer/locale string en_US.UTF-8\n\n# B.4.1. 本地化\nd-i debian-installer/locale string en_US.UTF-8\nd-i keyboard-configuration/xkb-keymap select us\n\n# B.4.2. 网络设置\nd-i netcfg/get_hostname string unassigned-hostname\nd-i netcfg/get_domain string unassigned-domain\nd-i netcfg/hostname string localhost\n\n# B.4.3. 网络控制台\n\n# B.4.4. 镜像设置\nd-i mirror/country string manual\n# d-i mirror/http/hostname string deb.debian.org\n\n# B.4.5. 帐号设置\nd-i passwd/make-user boolean false\n# 注意如果用 ssh key 后面还要删除密码\n# d-i passwd/root-password password ''\n# d-i passwd/root-password-again password ''\n# d-i passwd/root-password-crypted password ''\n# kali 需要下面这行，否则会提示输入用户名\nd-i passwd/root-login boolean true\n\n# B.4.6. 时钟与时区设置\nd-i time/zone string Asia/Shanghai\n\n# B.4.7. 分区\nd-i partman-auto/method string regular\nd-i partman-lvm/device_remove_lvm boolean true\nd-i partman-md/device_remove_md boolean true\nd-i partman-partitioning/confirm_write_new_label boolean true\nd-i partman/choose_partition select finish\nd-i partman/confirm boolean true\nd-i partman/confirm_nooverwrite boolean true\n\n# vm 原有系统是 bios + gpt，切换成 efi，用 iso 重装，需要确认此项\n# 用脚本重装的话，强制安装在第二个硬盘上也可能会遇到？\nd-i partman-efi/non_efi_system boolean true\n\n### Description: Do you want to return to the partitioning menu?\n#   You have not selected any partitions for use as swap space. Enabling swap\n#   space is recommended so that the system can make better use of the\n#   available physical memory, and so that it behaves better when physical\n#   memory is scarce. You may experience installation problems if you do not\n#   have enough physical memory.\n#   .\n#   If you do not go back to the partitioning menu and assign a swap partition,\n#   the installation will continue without swap space.\n# 坑的一比\n# 不是确认是否 no_swap\n# 而是 recipe no_swap 时，确认是否返回上一级重新分区\n# 选择 true 就一直死循环\nd-i partman-basicfilesystems/no_swap boolean false\n\n# 分区大小计算\n# https://salsa.debian.org/installer-team/partman-base/-/blob/master/lib/base.sh\n\n# 最小值 膨胀权重 最大值\n# https://salsa.debian.org/installer-team/partman-auto/-/blob/master/recipes/atomic?ref_type=heads\n# https://salsa.debian.org/installer-team/partman-auto/-/blob/master/recipes-amd64-efi/atomic?ref_type=heads\n# shellcheck disable=SC1083,SC2086,SC2154\nd-i partman-auto/expert_recipe_efi string efi :: \\\n    106 1 106 free \\\n    $iflabel{ gpt } method{ efi } format{ } . \\\n    1 1 -1 $default_filesystem \\\n    method{ format } format{ } use_filesystem{ } $default_filesystem{ } mountpoint{ / } .\n\n# 大于 2T 会自动用 gpt\n# shellcheck disable=SC1083,SC2086,SC2154\nd-i partman-auto/expert_recipe_bios string bios :: \\\n    1 1 1 free \\\n    $iflabel{ gpt } method{ biosgrub } . \\\n    1 1 -1 $default_filesystem \\\n    method{ format } format{ } use_filesystem{ } $default_filesystem{ } mountpoint{ / } .\n\n# B.4.8. 基本系统安装\n\n# B.4.9. 设置 apt\nd-i apt-setup/non-free boolean true\nd-i apt-setup/non-free-firmware boolean true\nd-i apt-setup/contrib boolean true\nd-i apt-setup/enable-source-repositories boolean false\n# kali 不要设置\n# d-i apt-setup/security_host string security.debian.org\n\n# B.4.10. 选择软件包\ntasksel tasksel/first multiselect ssh-server\nd-i pkgsel/upgrade select none\n\n# B.4.11. 安装 bootloader\n# 添加 bootx64.efi\nd-i grub-installer/force-efi-extra-removable boolean true\n\n# B.4.12. 完成安装\n# 由下面的 hold 2 设置\n# d-i finish-install/reboot_in_progress note\n\n# B.4.13. 预置其他的软件包\n\n# 其他设置\n# d-i anna/standard_modules boolean false\n# d-i anna/choose_modules string network-console\n# d-i network-console/password password ''\n# d-i network-console/password-again password ''\n\n# B.5.1. 安装过程中运行用户命令\n# 注意所有命令都会合并成一行命令\n\n# 最后的 true; \\ 没什么用，只是让 vscode 代码高亮不报错误\n\n# debian 11+ 才有 websocketd\n\n# 有 /cdrom/simple-cdd 才安装 simple-cdd-profiles\n# 不然安装时 control 脚本会报错：\n# Loading simple-cdd-profiles failed for unknown reasons\n\n# 未下载的组件，无法用 debconf-set，需要用 debconf-set-selections\n\n# https://salsa.debian.org/installer-team/network-console/-/blob/master/debian/network-console.postinst?ref_type=heads\n# https://salsa.debian.org/installer-team/user-setup/-/blob/master/user-setup-apply?ref_type=heads\n\n# 此时还没有配置源，anna-install 会在配置完源后再安装\nd-i preseed/early_command string true; \\\n    for str in $(grep -wo \"extra_[^ ]*\" /proc/cmdline | sed 's/^extra_//'); do eval \"$str\"; done; \\\n\n    di(){ \\\n        echo \"d-i $*\" >/tmp/selections.cfg; \\\n        echo \"d-i $*\" >>/tmp/selections.cfg.all; \\\n        debconf-set-selections /tmp/selections.cfg; \\\n        rm -f /tmp/selections.cfg; \\\n    }; \\\n\n    run_as_service_with_screen() { \\\n        if ! [ -f /etc/screenrc.bak ]; then \\\n            cp /etc/screenrc /etc/screenrc.bak; \\\n        fi; \\\n        true >/etc/screenrc; \\\n        screen sh -c 'while true; do pidof ${1##*/} || \"$@\"; sleep 5; done' _ \"$@\"; \\\n        cp -f /etc/screenrc.bak /etc/screenrc; \\\n    }; \\\n\n    if [ \"$hold\" = 1 ]; then \\\n        di auto-install/enable boolean false; \\\n        di debconf/priority select low; \\\n        di partman/early_command string; \\\n    else \\\n        { \\\n            echo 'Reinstalling...'; \\\n            echo 'Option 1. View logs:'; \\\n            echo '    tail -fn+1 /var/log/syslog'; \\\n            echo 'Option 2. Attach to the installer:'; \\\n            echo '    TERM=screen screen -xp1'; \\\n        } >>/etc/motd; \\\n        mem=$(grep ^MemTotal: /proc/meminfo | { read -r _ y _; echo \"$((y / 1024))\"; }); \\\n        if command -v websocketd && [ \"$mem\" -ge 400 ]; then \\\n            for _ in {1..10}; do \\\n                if wget \"$confhome/logviewer.html\" -O /tmp/index.html; then \\\n                    break; \\\n                fi; \\\n                sleep 5; \\\n            done; \\\n            if [ -z \"$web_port\" ]; then \\\n                web_port=80; \\\n            fi; \\\n            run_as_service_with_screen websocketd --port 80 --loglevel=fatal --staticdir=/tmp \\\n                sh -c \"tail -fn+0 /var/log/syslog | tr '\\r' '\\n' | grep -Fiv -e password -e token\" ; \\\n        fi; \\\n    fi; \\\n\n    if ! [ \"$hold\" = 2 ]; then \\\n        di finish-install/reboot_in_progress note; \\\n    fi; \\\n\n    if [ -s /configs/ssh_keys ]; then \\\n        di passwd/root-password-crypted password \"''\"; \\\n    else \\\n        di passwd/root-password-crypted password \"$(cat /configs/password-linux-sha512)\"; \\\n    fi; \\\n\n    mkdir -p /etc/ssh; \\\n    true >/etc/ssh/sshd_config; \\\n    if [ -s /configs/ssh_keys ]; then \\\n        (umask 077; mkdir -p /.ssh; cat /configs/ssh_keys >/.ssh/authorized_keys); \\\n    else \\\n        echo \"PermitRootLogin yes\" >>/etc/ssh/sshd_config; \\\n    fi; \\\n    if [ -n \"$ssh_port\" ] && ! [ \"$ssh_port\" = 22 ]; then \\\n        echo \"Port $ssh_port\" >>/etc/ssh/sshd_config; \\\n    fi; \\\n    grep -qs ^root: /etc/shadow || echo \"root:$(cat /configs/password-linux-sha512):1:0:99999:7:::\" >>/etc/shadow; \\\n    grep -qs ^nogroup: /etc/group || echo \"nogroup:*:65534:\" >>/etc/group; \\\n    grep -qs ^sshd: /etc/passwd || echo \"sshd:*:100:65534::/run/sshd:/bin/false\" >>/etc/passwd; \\\n    mkdir -p /run/sshd; \\\n    chmod 0755 /run/sshd; \\\n    ssh-keygen -A; \\\n    run_as_service_with_screen /usr/sbin/sshd -D; \\\n\n    if ls /configs/frpc.* >/dev/null 2>&1; then \\\n        url=$(sh /get-frpc-url.sh linux); \\\n        mkdir -p /usr/local/bin; \\\n        mkdir -p /usr/local/etc/frpc; \\\n        for _ in {1..10}; do \\\n            if wget -O- \"$url\" | tar xz \"*/frpc\" -O >/usr/local/bin/frpc; then \\\n                break; \\\n            fi; \\\n            sleep 5; \\\n        done; \\\n        chmod a+x /usr/local/bin/frpc; \\\n        cp /configs/frpc.* /usr/local/etc/frpc/; \\\n        run_as_service_with_screen /usr/local/bin/frpc -c /usr/local/etc/frpc/frpc.*; \\\n    fi; \\\n\n    if [ -d /cdrom/simple-cdd ]; then \\\n        anna-install simple-cdd-profiles; \\\n    fi\n\n# debian 11 initrd 没有 xargs awk\n# debian 12 initrd 没有 xargs\n# efi 分区大小未改变时，不会被格式化，因此需要手动删除旧系统的 efi 文件\n# os-prober 卡太久，因此跳过\nd-i partman/early_command string true; \\\n    eval \"$(grep -o 'extra_confhome=[^ ]*' /proc/cmdline | sed 's/^extra_//')\"; \\\n\n    postinst=/var/lib/dpkg/info/bootstrap-base.postinst; \\\n    cp $postinst $postinst.orig; \\\n    true >$postinst; \\\n\n    swapfile=/target/swapfile; \\\n    mem=$(grep ^MemTotal: /proc/meminfo | { read -r _ y _; echo \"$((y / 1024))\"; }); \\\n    swap_size=$((512 - mem)); \\\n    if [ $swap_size -gt 0 ]; then \\\n       echo \"fallocate -l ${swap_size}M $swapfile; mkswap $swapfile; swapon $swapfile\" >>$postinst; \\\n    fi; \\\n\n    echo \"swapoff -a; rm -f $swapfile\" >/usr/lib/finish-install.d/95swapoff; \\\n    chmod a+x /usr/lib/finish-install.d/95swapoff; \\\n\n    echo \"rm -rf /target/boot/efi/*; $postinst.orig\" >>$postinst; \\\n\n    xda=$(sh /get-xda.sh); \\\n    debconf-set partman-auto/disk \"/dev/$xda\"; \\\n    debconf-set grub-installer/bootdev \"/dev/$xda\"; \\\n    rm -rf /usr/sbin/fdisk /usr/sbin/sfdisk; \\\n\n    ttys=$(sh /ttys.sh console=); \\\n    debconf-set debian-installer/add-kernel-opts \"$ttys\"; \\\n\n    eths=$(cd /dev/netconf/ && ls); \\\n\n    if ! sh /can_use_cloud_kernel.sh \"$xda\" $eths; then \\\n        debconf-set base-installer/kernel/image \"$(debconf-get base-installer/kernel/image | sed 's/-cloud//')\"; \\\n    fi; \\\n\n    if [ -d /sys/firmware/efi ]; then \\\n        debconf-set partman-auto/expert_recipe \"$(debconf-get partman-auto/expert_recipe_efi)\"; \\\n    else \\\n        debconf-set partman-auto/expert_recipe \"$(debconf-get partman-auto/expert_recipe_bios)\"; \\\n    fi; \\\n\n    true >/bin/os-prober\n\n# kali ssh 默认关闭\n# 另一种方法处理 cloudcone\n# if [ \"$link_grub_dir\" = 1 ]; then mkdir /target/boot/grub2; echo 'chainloader (hd0)+1' >/target/boot/grub2/grub.cfg; fi; \\\n# debian 9 tar 不支持 --strip-components\nd-i preseed/late_command string true; \\\n    for str in $(grep -wo \"extra_[^ ]*\" /proc/cmdline | sed 's/^extra_//'); do eval \"$str\"; done; \\\n\n    if [ \"$elts\" = 1 ]; then sed -i \"s|deb\\.freexian\\.com/extended-lts|$deb_mirror|\" /target/etc/apt/sources.list; fi; \\\n\n    if [ \"$link_grub_dir\" = 1 ]; then ln -s grub /target/boot/grub2; fi; \\\n\n    in-target systemctl enable ssh; \\\n\n    if [ -s /configs/ssh_keys ]; then \\\n        (umask 077; mkdir -p /target/root/.ssh; cat /configs/ssh_keys >/target/root/.ssh/authorized_keys); \\\n        in-target passwd -d root; \\\n    else \\\n        echo \"PermitRootLogin yes\" >/target/etc/ssh/sshd_config.d/01-permitrootlogin.conf || \\\n        echo \"PermitRootLogin yes\" >>/target/etc/ssh/sshd_config; \\\n    fi; \\\n\n    if [ -n \"$ssh_port\" ] && ! [ \"$ssh_port\" = 22 ]; then \\\n        echo \"Port $ssh_port\" >/target/etc/ssh/sshd_config.d/01-change-ssh-port.conf || \\\n        echo \"Port $ssh_port\" >>/target/etc/ssh/sshd_config; \\\n    fi; \\\n\n    if ls /configs/frpc.* >/dev/null 2>&1; then \\\n        mkdir -p /target/usr/local/bin; \\\n        mkdir -p /target/usr/local/etc/frpc; \\\n        cp /usr/local/bin/frpc /target/usr/local/bin/; \\\n        cp /usr/local/etc/frpc/frpc.* /target/usr/local/etc/frpc/; \\\n        chmod a+x /target/usr/local/bin/frpc; \\\n        cp /frpc.service /target/etc/systemd/system/; \\\n        in-target systemctl enable frpc; \\\n    fi; \\\n\n    cp /fix-eth-name.sh /target/; \\\n    cp /fix-eth-name.service /target/etc/systemd/system/; \\\n    in-target systemctl enable fix-eth-name\n"
  },
  {
    "path": "fix-eth-name.initd",
    "content": "#!/sbin/openrc-run\n\nDescription=\"Fix Eth Name\"\n\n# https://gitlab.alpinelinux.org/alpine/aports/-/blob/master/main/openrc/networking.initd\n# https://gitlab.alpinelinux.org/alpine/aports/-/blob/master/main/dhcpcd/dhcpcd.initd\ndepend() {\n    need localmount\n    want dev-settle\n\n    after bootmisc hwdrivers modules\n    before net networking dhcpcd\n}\n\nstart() {\n    ebegin \"Fix Eth Name\"\n    ash /fix-eth-name.sh\n    eend $?\n}\n\nstart_post() {\n    rc-service fix-eth-name zap\n    rc-update del fix-eth-name boot\n    rm -f /etc/init.d/fix-eth-name\n    rm -f /fix-eth-name.sh\n}\n"
  },
  {
    "path": "fix-eth-name.service",
    "content": "[Unit]\nDescription=Fix Eth Name\nConditionPathExists=/fix-eth-name.sh\n\nAfter=dbus.service\n\nBefore=cloud-init-local.service\nBefore=network.service\nBefore=networking.service\nBefore=systemd-networkd.service\nBefore=NetworkManager.service\nBefore=wickedd-auto4.service\nBefore=wickedd-dhcp4.service\nBefore=wickedd-dhcp6.service\nBefore=wickedd.service\n\nBefore=network.target\n\n[Service]\nType=oneshot\nExecStart=/usr/bin/env bash /fix-eth-name.sh\nExecStart=/usr/bin/env rm -f /fix-eth-name.sh\nExecStart=/usr/bin/env rm -f /etc/systemd/system/fix-eth-name.service\nExecStart=/usr/bin/env rm -f /etc/systemd/system/multi-user.target.wants/fix-eth-name.service\nExecStart=/usr/bin/env rm -f /lib/systemd/system-preset/01-fix-eth-name.preset\nExecStart=/usr/bin/env rm -f /usr/lib/systemd/system-preset/01-fix-eth-name.preset\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "fix-eth-name.sh",
    "content": "#!/usr/bin/env bash\n# shellcheck shell=dash\n# shellcheck disable=SC3001,SC3010\n# alpine 使用 busybox ash\n\nset -eE\n\n# openeuler 需等待 udev 将网卡名从 eth0 改为 enp3s0\nsleep 10\n# 不知道有没有用\nif command -v udevadm >/dev/null; then\n    # udevadm trigger\n    udevadm settle\nelif command -v mdev >/dev/null; then\n    mdev -sf\nfi\n\n# 本脚本在首次进入新系统后运行\n# 将 trans 阶段生成的网络配置中的网卡名(eth0) 改为正确的网卡名，也适用于以下情况\n# 1. alpine 要运行此脚本，因为安装后的内核可能有 netboot 没有的驱动\n# 2. dmit debian 普通内核(安装时)和云内核网卡名不一致\n#    https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=928923\n\n# todo: 删除 cloud-init\n\nto_lower() {\n    tr '[:upper:]' '[:lower:]'\n}\n\nretry() {\n    local max_try=$1\n    shift\n\n    for i in $(seq \"$max_try\"); do\n        if \"$@\"; then\n            return\n        else\n            ret=$?\n            if [ \"$i\" -ge \"$max_try\" ]; then\n                return $ret\n            fi\n            sleep 1\n        fi\n    done\n}\n\n# openeuler 本脚本运行一秒后才有 enp3s0\n# 用 systemd-analyze plot >a.svg 发现 sys-subsystem-net-devices-enp3s0.device 也是出现在 NetworkManager 之后\n# 因此需要等待网卡出现\nget_ethx_by_mac() {\n    retry 10 _get_ethx_by_mac \"$@\"\n}\n\n_get_ethx_by_mac() {\n    mac=$(echo \"$1\" | to_lower)\n\n    flag=$2\n    if [ -z \"$flag\" ]; then\n        flag=master\n    fi\n\n    if true; then\n        if [ \"$flag\" = master ]; then\n            # master\n            # 过滤 azure vf (带 master ethx)\n            ip -o link | grep -i \"$mac\" | grep -v master | awk '{print $2}' | cut -d: -f1 | grep .\n        else\n            # slave\n            # 带 master ethx\n            ip -o link | grep -i \"$mac\" | grep -w master | awk '{print $2}' | cut -d: -f1 | grep .\n        fi\n    else\n        for i in $(cd /sys/class/net && echo *); do\n            if [ \"$(cat \"/sys/class/net/$i/address\")\" = \"$mac\" ]; then\n                if [ $(($(cat \"/sys/class/net/$i/flags\") & 0x800)) -ne 0 ]; then\n                    fact_flag=slave\n                else\n                    fact_flag=master\n                fi\n                if [ \"$flag\" = \"$fact_flag\" ]; then\n                    echo \"$i\"\n                    return\n                fi\n            fi\n        done\n        return 1\n    fi\n}\n\nfix_rh_sysconfig() {\n    for file in /etc/sysconfig/network-scripts/ifcfg-eth*; do\n        # 没有 ifcfg-eth* 也会执行一次，因此要判断文件是否存在\n        [ -f \"$file\" ] || continue\n        mac=$(grep ^HWADDR= \"$file\" | cut -d= -f2 | grep .) || continue\n        ethx=$(get_ethx_by_mac \"$mac\") || continue\n\n        proper_file=/etc/sysconfig/network-scripts/ifcfg-$ethx\n        if [ \"$file\" != \"$proper_file\" ]; then\n            # 更改文件内容\n            sed -i \"s/^DEVICE=.*/DEVICE=$ethx/\" \"$file\"\n\n            # 不要直接更改文件名，因为可能覆盖已有文件\n            mv \"$file\" \"$proper_file.tmp\"\n        fi\n    done\n\n    # 更改文件名\n    for tmp_file in /etc/sysconfig/network-scripts/ifcfg-e*.tmp; do\n        if [ -f \"$tmp_file\" ]; then\n            mv \"$tmp_file\" \"${tmp_file%.tmp}\"\n        fi\n    done\n}\n\nfix_suse_sysconfig() {\n    for file in /etc/sysconfig/network/ifcfg-eth*; do\n        [ -f \"$file\" ] || continue\n\n        # 可能两边有引号\n        mac=$(grep ^LLADDR= \"$file\" | cut -d= -f2 | sed \"s/'//g\" | grep .) || continue\n        ethx=$(get_ethx_by_mac \"$mac\") || continue\n\n        old_ethx=${file##*-}\n        if ! [ \"$old_ethx\" = \"$ethx\" ]; then\n            # 不要直接更改文件名，因为可能覆盖已有文件\n            for type in ifcfg ifroute; do\n                old_file=/etc/sysconfig/network/$type-$old_ethx\n                new_file=/etc/sysconfig/network/$type-$ethx.tmp\n                # 防止没有 ifroute-eth* 导致中断脚本\n                if [ -f \"$old_file\" ]; then\n                    mv \"$old_file\" \"$new_file\"\n                fi\n            done\n        fi\n    done\n\n    # 上面的循环结束后，再将 tmp 改成正式文件\n    for tmp_file in \\\n        /etc/sysconfig/network/ifcfg-e*.tmp \\\n        /etc/sysconfig/network/ifroute-e*.tmp; do\n        if [ -f \"$tmp_file\" ]; then\n            mv \"$tmp_file\" \"${tmp_file%.tmp}\"\n        fi\n    done\n}\n\nfix_network_manager() {\n    for file in /etc/NetworkManager/system-connections/cloud-init-eth*.nmconnection; do\n        [ -f \"$file\" ] || continue\n        mac=$(grep ^mac-address= \"$file\" | cut -d= -f2 | grep .) || continue\n        ethx=$(get_ethx_by_mac \"$mac\") || continue\n\n        proper_file=/etc/NetworkManager/system-connections/$ethx.nmconnection\n\n        # 更改文件内容\n        sed -i \"s/^id=.*/id=$ethx/\" \"$file\"\n\n        # 更改文件名\n        mv \"$file\" \"$proper_file\"\n\n        # NM 不会自动忽略 Azure 的 slave 网卡，需手动设置\n        # azure 文档中的方法不够通用，只适合 azure\n        # https://learn.microsoft.com/zh-cn/azure/virtual-network/accelerated-networking-overview\n\n        # 我们采用红帽的方法\n        # 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\n        if slave_ethx=$(get_ethx_by_mac \"$mac\" slave); then\n            cat >\"/etc/NetworkManager/conf.d/99-$slave_ethx-unmanaged.conf\" <<EOF\n[device-$slave_ethx-unmanaged]\nmatch-device=interface-name:$slave_ethx\nmanaged=0\nEOF\n        fi\n\n        # 也可以设置 unmanaged-devices, 但是官方文档不推荐\n        # https://networkmanager.pages.freedesktop.org/NetworkManager/NetworkManager/NetworkManager.conf.html#:~:text=may%20be%20a-,better%20choice,-.\n    done\n}\n\n# debian 9 IPV6 onlink 路由需要 post-up\n\n# auto lo\n# iface lo inet loopback\n\n# # mac 11:22:33:44:55:66    # 用此行匹配网卡\n# auto eth0\n# iface eth0 inet static\n#     address 1.1.1.1/25\n#     gateway 1.1.1.1\n#     dns-nameservers 1.1.1.1\n#     dns-nameservers 8.8.8.8\n# iface eth0 inet6 static\n#     address 2602:1:0:80::100/64\n#     gateway 2602:1:0:80::1\n#     post-up ip route add 2602:1:0:80::1 dev eth0\n#     post-up ip route add default via 2602:1:0:80::1 dev eth0\n#     dns-nameserver 2606:4700:4700::1111\n#     dns-nameserver 2001:4860:4860::8888\n\nfix_ifupdown() {\n    file=/etc/network/interfaces\n    tmp_file=$file.tmp\n\n    rm -f \"$tmp_file\"\n\n    if [ -f \"$file\" ]; then\n        while IFS= read -r line; do\n            del_this_line=false\n            if [[ \"$line\" = \"# mac \"* ]]; then\n                ethx=\n                if mac=$(echo \"$line\" | awk '{print $NF}'); then\n                    ethx=$(get_ethx_by_mac \"$mac\") || true\n                fi\n                del_this_line=true\n            elif [[ \"$line\" = \"iface e\"* ]] ||\n                [[ \"$line\" = \"auto e\"* ]] ||\n                [[ \"$line\" = \"allow-hotplug e\"* ]]; then\n                if [ -n \"$ethx\" ]; then\n                    line=$(echo \"$line\" | awk \"{\\$2=\\\"$ethx\\\"; print \\$0}\")\n                fi\n            elif [[ \"$line\" = *\" dev e\"* ]]; then\n                if [ -n \"$ethx\" ]; then\n                    # awk 会去除前面的空格\n                    line=$(echo \"$line\" | sed -E \"s/[^ ]*$/$ethx/\")\n                fi\n            fi\n            if ! $del_this_line; then\n                echo \"$line\" >>\"$tmp_file\"\n            fi\n        done <\"$file\"\n\n        mv \"$tmp_file\" \"$file\"\n    fi\n}\n\nfix_netplan() {\n    file=/etc/netplan/50-cloud-init.yaml\n    tmp_file=$file.tmp\n\n    rm -f \"$tmp_file\"\n\n    if [ -f \"$file\" ]; then\n        while IFS= read -r line; do\n            if echo \"$line\" | grep -Eq '^[[:space:]]+macaddress:'; then\n                # 得到正确的网卡名\n                mac=$(echo \"$line\" | awk '{print $NF}' | sed 's/\"//g')\n                ethx=$(get_ethx_by_mac \"$mac\") || true\n            elif echo \"$line\" | grep -Eq '^[[:space:]]+eth[0-9]+:'; then\n                # 改成正确的网卡名\n                if [ -n \"$ethx\" ]; then\n                    line=$(echo \"$line\" | sed -E \"s/[^[:space:]]+/$ethx:/\")\n                fi\n            fi\n            echo \"$line\" >>\"$tmp_file\"\n\n            # 删除 set-name 不过这一步在 trans 已完成\n            # 因为 netplan-generator 会在 systemd generator 阶段就根据 netplan 配置重命名网卡\n            # systemd generator 阶段比本脚本和 systemd-networkd 更早运行\n\n            # 倒序\n        done < <(grep -Ev \"^[[:space:]]+set-name:\" \"$file\" | tac)\n\n        # 再倒序回来\n        tac \"$tmp_file\" >\"$file\"\n        rm -f \"$tmp_file\"\n\n        # 通过 systemd netplan generator 生成 /run/systemd/network/10-netplan-enp3s0.network\n        systemctl daemon-reload\n    fi\n}\n\nfix_systemd_networkd() {\n    for file in /etc/systemd/network/10-cloud-init-eth*.network; do\n        [ -f \"$file\" ] || continue\n        mac=$(grep ^MACAddress= \"$file\" | cut -d= -f2 | grep .) || continue\n        ethx=$(get_ethx_by_mac \"$mac\") || continue\n\n        proper_file=/etc/systemd/network/10-$ethx.network\n\n        # 更改文件内容\n        sed -Ei \"s/^Name=eth[0-9]+/Name=$ethx/\" \"$file\"\n\n        # 更改文件名\n        mv \"$file\" \"$proper_file\"\n    done\n}\n\nfix_rh_sysconfig\nfix_suse_sysconfig\nfix_network_manager\nfix_ifupdown\nfix_netplan\nfix_systemd_networkd\n"
  },
  {
    "path": "frpc-example.toml",
    "content": "serverAddr = \"11.22.33.44\"\nserverPort = 7000\nauth.token = \"123456\"\n\n[[proxies]]\nname = \"ssh\"\ntype = \"tcp\"\nlocalIP = \"127.0.0.1\"\nlocalPort = 22\nremotePort = 2222\n\n[[proxies]]\nname = \"rdp_tcp\"\ntype = \"tcp\"\nlocalIP = \"127.0.0.1\"\nlocalPort = 3389\nremotePort = 33890\n\n[[proxies]]\nname = \"rdp_udp\"\ntype = \"udp\"\nlocalIP = \"127.0.0.1\"\nlocalPort = 3389\nremotePort = 33890\n"
  },
  {
    "path": "frpc.service",
    "content": "# https://github.com/archlinuxcn/repo/blob/master/archlinuxcn/frp/frpc.service\n\n[Unit]\nDescription=Frp Client Service\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nUser=nobody\nRestart=on-failure\nRestartSec=5s\nExecStart=/usr/local/bin/frpc -c /usr/local/etc/frpc/frpc.conf\nExecReload=/usr/local/bin/frpc reload -c /usr/local/etc/frpc/frpc.conf\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "get-frpc-url.sh",
    "content": "#!/bin/ash\n# shellcheck shell=dash\n# trans.sh/debian.cfg 共用此脚本\n\n# debian 9 不支持 set -E\nset -e\n\nis_in_china() {\n    grep -q 1 /dev/netconf/*/is_in_china\n}\n\nis_ipv6_only() {\n    ! grep -q 1 /dev/netconf/*/ipv4_has_internet\n}\n\nget_frpc_url() {\n    # 传入 windows 或者 linux\n    local os_type=$1\n    local nt_ver=$2\n    local os_bit=${3:-64}\n\n    get_old_version() {\n        # 脚本不支持安装 32 位 linux 系统，因此不用管\n        if [ \"$os_type\" = windows ]; then\n            # 最早支持 toml 的版本是 0.52.0\n\n            # 最后支持 vista 的版本是 0.29.0\n            # 最后支持 32 位的版本是 0.51.3\n            # 最后支持 win7 的版本是 0.54.0\n            case \"$os_bit\" in\n            32)\n                case \"$nt_ver\" in\n                6.0) echo 0.29.0 ;; # vista\n                *) echo 0.51.3 ;;   # win7+\n                esac\n                ;;\n            64)\n                case \"$nt_ver\" in\n                6.0) echo 0.29.0 ;; # vista\n                6.1) echo 0.54.0 ;; # win7\n                # 目前最新版本 v0.66.0 依然可以在 win8 上运行\n                esac\n                ;;\n            esac\n        fi\n    }\n\n    is_need_old_version() {\n        [ -n \"$(get_old_version)\" ]\n    }\n\n    version=$(\n        if is_need_old_version; then\n            get_old_version\n        else\n            # debian 11 initrd 没有 xargs awk\n            # debian 12 initrd 没有 xargs\n            # github 不支持 ipv6\n            if is_in_china || is_ipv6_only; then\n                wget -O- https://mirrors.nju.edu.cn/github-release/fatedier/frp/LatestRelease/frp_sha256_checksums.txt |\n                    grep -m1 frp_ | cut -d_ -f2\n            else\n                # https://api.github.com/repos/fatedier/frp/releases/latest 有请求次数限制\n\n                # root@localhost:~# wget --spider -S https://github.com/fatedier/frp/releases/latest 2>&1 | grep Location:\n                #   Location: https://github.com/fatedier/frp/releases/tag/v0.62.0\n                # Location: https://github.com/fatedier/frp/releases/tag/v0.62.0 [following]  # 原版 wget 多了这行\n\n                wget --spider -S https://github.com/fatedier/frp/releases/latest 2>&1 |\n                    grep -m1 '^  Location:' | sed 's,.*/tag/v,,'\n            fi\n        fi\n    )\n\n    if [ -z \"$version\" ]; then\n        echo 'cannot find version' >&2\n        return 1\n    fi\n\n    suffix=$(\n        case \"$os_type\" in\n        linux) echo tar.gz ;;\n        windows) echo zip ;;\n        esac\n    )\n\n    mirror=$(\n        # nju 没有 win7 用的旧版\n        # github 不支持 ipv6\n        # daocloud 加速不支持 ipv6\n        # jsdelivr 不支持 github releases 文件\n        if is_ipv6_only; then\n            if is_need_old_version; then\n                echo 'NOT_SUPPORT' >&2\n                return 1\n            else\n                echo https://mirrors.nju.edu.cn/github-release/fatedier/frp\n            fi\n        else\n            if is_in_china; then\n                if is_need_old_version; then\n                    echo https://files.m.daocloud.io/github.com/fatedier/frp/releases/download\n                else\n                    echo https://mirrors.nju.edu.cn/github-release/fatedier/frp\n                fi\n            else\n                echo https://github.com/fatedier/frp/releases/download\n            fi\n        fi\n    )\n\n    arch=$(\n        case \"$(uname -m)\" in\n        x86_64)\n            case \"$os_bit\" in\n            32) echo 386 ;;\n            64) echo amd64 ;;\n            esac\n            ;;\n        aarch64) echo arm64 ;;\n        esac\n    )\n\n    filename=frp_${version}_${os_type}_${arch}.$suffix\n\n    echo \"${mirror}/v${version}/${filename}\"\n}\n\nget_frpc_url \"$@\"\n"
  },
  {
    "path": "get-xda.sh",
    "content": "#!/bin/sh\n# debian ubuntu redhat 安装模式共用此脚本\n# alpine 未用到此脚本\n\nget_all_disks() {\n    # shellcheck disable=SC2010\n    ls /sys/block/ | grep -Ev '^(loop|sr|nbd)'\n}\n\nget_xda() {\n    # 如果没找到 main_disk 或 xda\n    # 返回假的值，防止意外地格式化全部盘\n    eval \"$(grep -o 'extra_main_disk=[^ ]*' /proc/cmdline | sed 's/^extra_//')\"\n\n    if [ -z \"$main_disk\" ]; then\n        echo 'MAIN_DISK_NOT_FOUND'\n        return 1\n    fi\n\n    for disk in $(get_all_disks); do\n        if fdisk -l \"/dev/$disk\" | grep -iq \"$main_disk\"; then\n            echo \"$disk\"\n            return\n        fi\n    done\n\n    echo 'XDA_NOT_FOUND'\n    return 1\n}\n\nget_xda\n"
  },
  {
    "path": "initrd-network.sh",
    "content": "#!/bin/ash\n# shellcheck shell=dash\n# alpine/debian initrd 共用此脚本\n\n# accept_ra 接收 RA + 自动配置网关\n# autoconf  自动配置地址，依赖 accept_ra\n\nmac_addr=$1\nipv4_addr=$2\nipv4_gateway=$3\nipv6_addr=$4\nipv6_gateway=$5\nis_in_china=$6\nipv6_extra_addrs=$7\n\nDHCP_TIMEOUT=15\nDNS_FILE_TIMEOUT=5\nTEST_TIMEOUT=10\n\n# 检测是否有网络是通过检测这些 IP 的端口是否开放\n# 因为 debian initrd 没有 nslookup\n# 改成 generate_204？但检测网络时可能 resolv.conf 为空\n# HTTP 80\n# HTTPS/DOH 443\n# DOT 853\nif $is_in_china; then\n    ipv4_dns1='223.5.5.5'\n    ipv4_dns2='119.29.29.29' # 不开放 853\n    ipv6_dns1='2400:3200::1'\n    ipv6_dns2='2402:4e00::' # 不开放 853\nelse\n    ipv4_dns1='1.1.1.1'\n    ipv4_dns2='8.8.8.8' # 不开放 80\n    ipv6_dns1='2606:4700:4700::1111'\n    ipv6_dns2='2001:4860:4860::8888' # 不开放 80\nfi\n\n# 找到主网卡\n# debian 11 initrd 没有 xargs awk\n# debian 12 initrd 没有 xargs\nget_ethx() {\n    # 过滤 azure vf (带 master ethx)\n    # 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000\\    link/ether 60:45:bd:21:8a:51 brd ff:ff:ff:ff:ff:ff\n    # 3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP800> mtu 1500 qdisc mq master eth0 state UP qlen 1000\\    link/ether 60:45:bd:21:8a:51 brd ff:ff:ff\n    if false; then\n        ip -o link | grep -i \"$mac_addr\" | grep -v master | awk '{print $2}' | cut -d: -f1 | grep .\n    else\n        ip -o link | grep -i \"$mac_addr\" | grep -v master | cut -d' ' -f2 | cut -d: -f1 | grep .\n    fi\n}\n\nget_ipv4_gateway() {\n    # debian 11 initrd 没有 xargs awk\n    # debian 12 initrd 没有 xargs\n    ip -4 route show default dev \"$ethx\" | head -1 | cut -d ' ' -f3\n}\n\nget_ipv6_gateway() {\n    # debian 11 initrd 没有 xargs awk\n    # debian 12 initrd 没有 xargs\n    ip -6 route show default dev \"$ethx\" | head -1 | cut -d ' ' -f3\n}\n\nget_first_ipv4_addr() {\n    # debian 11 initrd 没有 xargs awk\n    # debian 12 initrd 没有 xargs\n    if false; then\n        ip -4 -o addr show scope global dev \"$ethx\" | head -1 | awk '{print $4}'\n    else\n        ip -4 -o addr show scope global dev \"$ethx\" | head -1 | grep -o '[0-9\\.]*/[0-9]*'\n    fi\n}\n\nget_first_ipv4_gateway() {\n    # debian 11 initrd 没有 xargs awk\n    # debian 12 initrd 没有 xargs\n    if false; then\n        ip -4 route show default dev \"$ethx\" | head -1 | awk '{print $3}'\n    else\n        ip -4 route show default dev \"$ethx\" | head -1 | cut -d' ' -f3\n    fi\n}\n\nremove_netmask() {\n    cut -d/ -f1\n}\n\nget_first_ipv6_addr() {\n    # debian 11 initrd 没有 xargs awk\n    # debian 12 initrd 没有 xargs\n    if false; then\n        ip -6 -o addr show scope global dev \"$ethx\" | head -1 | awk '{print $4}'\n    else\n        ip -6 -o addr show scope global dev \"$ethx\" | head -1 | grep -o '[0-9a-f\\:]*/[0-9]*'\n    fi\n}\n\nget_first_ipv6_gateway() {\n    # debian 11 initrd 没有 xargs awk\n    # debian 12 initrd 没有 xargs\n    if false; then\n        ip -6 route show default dev \"$ethx\" | head -1 | awk '{print $3}'\n    else\n        ip -6 route show default dev \"$ethx\" | head -1 | cut -d' ' -f3\n    fi\n}\n\nis_have_ipv4_addr() {\n    ip -4 addr show scope global dev \"$ethx\" | grep -q inet\n}\n\nis_have_ipv6_addr() {\n    ip -6 addr show scope global dev \"$ethx\" | grep -q inet6\n}\n\nis_have_ipv4_gateway() {\n    ip -4 route show default dev \"$ethx\" | grep -q .\n}\n\nis_have_ipv6_gateway() {\n    ip -6 route show default dev \"$ethx\" | grep -q .\n}\n\nis_have_ipv4() {\n    is_have_ipv4_addr && is_have_ipv4_gateway\n}\n\nis_have_ipv6() {\n    is_have_ipv6_addr && is_have_ipv6_gateway\n}\n\nis_have_ipv4_dns() {\n    [ -f /etc/resolv.conf ] && grep -q '^nameserver .*\\.' /etc/resolv.conf\n}\n\nis_have_ipv6_dns() {\n    [ -f /etc/resolv.conf ] && grep -q '^nameserver .*:' /etc/resolv.conf\n}\n\nadd_missing_ipv4_config() {\n    if [ -n \"$ipv4_addr\" ] && [ -n \"$ipv4_gateway\" ]; then\n        if ! is_have_ipv4_addr; then\n            ip -4 addr add \"$ipv4_addr\" dev \"$ethx\"\n        fi\n\n        if ! is_have_ipv4_gateway; then\n            # 如果 dhcp 无法设置onlink网关，那么在这里设置\n            # debian 9 ipv6 不能识别 onlink，但 ipv4 能识别 onlink\n            if true; then\n                ip -4 route add \"$ipv4_gateway\" dev \"$ethx\"\n                ip -4 route add default via \"$ipv4_gateway\" dev \"$ethx\"\n            else\n                ip -4 route add default via \"$ipv4_gateway\" dev \"$ethx\" onlink\n            fi\n        fi\n    fi\n}\n\nadd_missing_ipv6_config() {\n    if [ -n \"$ipv6_addr\" ] && [ -n \"$ipv6_gateway\" ]; then\n        if ! is_have_ipv6_addr; then\n            ip -6 addr add \"$ipv6_addr\" dev \"$ethx\"\n        fi\n\n        if ! is_have_ipv6_gateway; then\n            # 如果 dhcp 无法设置onlink网关，那么在这里设置\n            # debian 9 ipv6 不能识别 onlink\n            if true; then\n                ip -6 route add \"$ipv6_gateway\" dev \"$ethx\"\n                ip -6 route add default via \"$ipv6_gateway\" dev \"$ethx\"\n            else\n                ip -6 route add default via \"$ipv6_gateway\" dev \"$ethx\" onlink\n            fi\n        fi\n\n        # 添加额外的 IPv6 地址（逗号分隔）\n        if [ -n \"$ipv6_extra_addrs\" ]; then\n            printf '%s\\n' \"$ipv6_extra_addrs\" | tr ',' '\\n' | while IFS= read -r addr; do\n                if [ -n \"$addr\" ]; then\n                    ip -6 addr add \"$addr\" dev \"$ethx\" 2>/dev/null || true\n                fi\n            done\n        fi\n    fi\n}\n\nis_need_test_ipv4() {\n    is_have_ipv4 && ! $ipv4_has_internet\n}\n\nis_need_test_ipv6() {\n    is_have_ipv6 && ! $ipv6_has_internet\n}\n\n# 测试方法：\n# ping   有的机器禁止\n# nc     测试 dot doh 端口是否开启\n# wget   测试下载\n\n# initrd 里面的软件版本，是否支持指定源IP/网卡\n# 软件     nc  wget  nslookup\n# debian9  ×    √   没有此软件\n# alpine   √    ×      ×\n\ntest_by_wget() {\n    src=$1\n    dst=$2\n\n    # ipv6 需要添加 []\n    if echo \"$dst\" | grep -q ':'; then\n        url=\"https://[$dst]\"\n    else\n        url=\"https://$dst\"\n    fi\n\n    # tcp 443 通了就算成功，不管 http 是不是 404\n    # grep -m1 快速返回\n    wget -T \"$TEST_TIMEOUT\" \\\n        --bind-address=\"$src\" \\\n        --no-check-certificate \\\n        --max-redirect 0 \\\n        --tries 1 \\\n        -O /dev/null \\\n        \"$url\" 2>&1 | grep -iq -m1 connected\n}\n\ntest_by_nc() {\n    src=$1\n    dst=$2\n\n    # tcp 443 通了就算成功\n    nc -z -v \\\n        -w \"$TEST_TIMEOUT\" \\\n        -s \"$src\" \\\n        \"$dst\" 443\n}\n\nis_debian_kali() {\n    [ -f /etc/lsb-release ] && grep -Eiq 'Debian|Kali' /etc/lsb-release\n}\n\ntest_connect() {\n    if is_debian_kali; then\n        test_by_wget \"$1\" \"$2\"\n    else\n        test_by_nc \"$1\" \"$2\"\n    fi\n}\n\ntest_internet() {\n    for i in $(seq 5); do\n        echo \"Testing Internet Connection. Test $i... \"\n        if is_need_test_ipv4 &&\n            current_ipv4_addr=\"$(get_first_ipv4_addr | remove_netmask)\" &&\n            { test_connect \"$current_ipv4_addr\" \"$ipv4_dns1\" ||\n                test_connect \"$current_ipv4_addr\" \"$ipv4_dns2\"; } >/dev/null 2>&1; then\n            echo \"IPv4 has internet.\"\n            ipv4_has_internet=true\n        fi\n        if is_need_test_ipv6 &&\n            current_ipv6_addr=\"$(get_first_ipv6_addr | remove_netmask)\" &&\n            { test_connect \"$current_ipv6_addr\" \"$ipv6_dns1\" ||\n                test_connect \"$current_ipv6_addr\" \"$ipv6_dns2\"; } >/dev/null 2>&1; then\n            echo \"IPv6 has internet.\"\n            ipv6_has_internet=true\n        fi\n        if ! is_need_test_ipv4 && ! is_need_test_ipv6; then\n            break\n        fi\n        sleep 1\n    done\n}\n\nflush_ipv4_config() {\n    ip -4 addr flush scope global dev \"$ethx\"\n    ip -4 route flush dev \"$ethx\"\n    # DHCP 获取的 IP 不是重装前的 IP 时，一并删除 DHCP 获取的 DNS，以防 DNS 无效\n    sed -i \"/\\./d\" /etc/resolv.conf\n}\n\nshould_disable_dhcpv4=false\nshould_disable_accept_ra=false\nshould_disable_autoconf=false\n\nflush_ipv6_config() {\n    if $should_disable_accept_ra; then\n        echo 0 >\"/proc/sys/net/ipv6/conf/$ethx/accept_ra\"\n    fi\n    if $should_disable_autoconf; then\n        echo 0 >\"/proc/sys/net/ipv6/conf/$ethx/autoconf\"\n    fi\n    ip -6 addr flush scope global dev \"$ethx\"\n    ip -6 route flush dev \"$ethx\"\n    # DHCP 获取的 IP 不是重装前的 IP 时，一并删除 DHCP 获取的 DNS，以防 DNS 无效\n    sed -i \"/:/d\" /etc/resolv.conf\n}\n\nfor i in $(seq 20); do\n    if ethx=$(get_ethx); then\n        break\n    fi\n    sleep 1\ndone\n\nif [ -z \"$ethx\" ]; then\n    echo \"Not found network card: $mac_addr\"\n    exit\nfi\n\necho \"Configuring $ethx ($mac_addr)...\"\n\n# 不开启 lo 则 frp 无法连接 127.0.0.1 22\nip link set dev lo up\n\n# 开启 ethx\nip link set dev \"$ethx\" up\nsleep 1\n\n# 开启 dhcpv4/v6\n# debian / kali\nif [ -f /usr/share/debconf/confmodule ]; then\n    # shellcheck source=/dev/null\n    . /usr/share/debconf/confmodule\n\n    db_progress STEP 1\n\n    # dhcpv4\n    # 无需等待写入 dns，在 dhcpv6 等待\n    db_progress INFO netcfg/dhcp_progress\n    udhcpc -i \"$ethx\" -f -q -n || true\n    db_progress STEP 1\n\n    # slaac + dhcpv6\n    db_progress INFO netcfg/slaac_wait_title\n    # https://salsa.debian.org/installer-team/netcfg/-/blob/master/autoconfig.c#L148\n    cat <<EOF >/var/lib/netcfg/dhcp6c.conf\ninterface $ethx {\n    send ia-na 0;\n    request domain-name-servers;\n    request domain-name;\n    script \"/lib/netcfg/print-dhcp6c-info\";\n};\n\nid-assoc na 0 {\n};\nEOF\n    dhcp6c -c /var/lib/netcfg/dhcp6c.conf \"$ethx\" || true\n    sleep $DHCP_TIMEOUT # 等待获取 ip 和写入 dns\n    # kill-all-dhcp\n    kill -9 \"$(cat /var/run/dhcp6c.pid)\" || true\n    db_progress STEP 1\n\n    # 静态 + 检测网络提示\n    db_subst netcfg/link_detect_progress interface \"$ethx\"\n    db_progress INFO netcfg/link_detect_progress\nelse\n    # alpine\n    # h3c 移动云电脑使用 udhcpc 会重复提示 sending select，因此添加 timeout 强制结束进程\n    # dhcpcd 会配置租约时间，过期会移除 IP，但我们的没有在后台运行 dhcpcd ，因此用 udhcpc\n    method=udhcpc\n\n    case \"$method\" in\n    udhcpc)\n        timeout $DHCP_TIMEOUT udhcpc -i \"$ethx\" -f -q -n || true\n        timeout $DHCP_TIMEOUT udhcpc6 -i \"$ethx\" -f -q -n || true\n        sleep $DNS_FILE_TIMEOUT # 好像不用等待写入 dns，但是以防万一\n        ;;\n    dhcpcd)\n        # https://gitlab.alpinelinux.org/alpine/aports/-/blob/master/main/dhcpcd/dhcpcd.pre-install\n        grep -q dhcpcd /etc/group || addgroup -S dhcpcd\n        grep -q dhcpcd /etc/passwd || adduser -S -D -H \\\n            -h /var/lib/dhcpcd \\\n            -s /sbin/nologin \\\n            -G dhcpcd \\\n            -g dhcpcd \\\n            dhcpcd\n\n        # --noipv4ll 禁止生成 169.254.x.x\n        if false; then\n            # 等待 DHCP 全过程\n            timeout $DHCP_TIMEOUT \\\n                dhcpcd --persistent --noipv4ll --nobackground \"$ethx\"\n        else\n            # 等待 DNS\n            dhcpcd --persistent --noipv4ll \"$ethx\" # 获取到 IP 后立即切换到后台\n            sleep $DNS_FILE_TIMEOUT                # 需要等待写入 dns\n            dhcpcd -x \"$ethx\"                      # 终止\n        fi\n        # autoconf 和 accept_ra 会被 dhcpcd 自动关闭，因此需要重新打开\n        # 如果没重新打开，重新运行 dhcpcd 命令依然可以正常生成 slaac 地址和路由\n        sysctl -w \"net.ipv6.conf.$ethx.autoconf=1\"\n        sysctl -w \"net.ipv6.conf.$ethx.accept_ra=1\"\n        ;;\n    esac\nfi\n\n# 等待slaac\n# 有ipv6地址就跳过，不管是slaac或者dhcpv6\n# 因为会在trans里判断\n# 这里等待5秒就够了，因为之前尝试获取dhcp6也用了一段时间\nfor i in $(seq 5 -1 0); do\n    is_have_ipv6 && break\n    echo \"waiting slaac for ${i}s\"\n    sleep 1\ndone\n\n# 记录是否有动态地址\n# 由于还没设置静态ip，所以有条目表示有动态地址\nis_have_ipv4_addr && dhcpv4=true || dhcpv4=false\nis_have_ipv6_addr && dhcpv6_or_slaac=true || dhcpv6_or_slaac=false\nis_have_ipv6_gateway && ra_has_gateway=true || ra_has_gateway=false\n\n# 如果自动获取的 IP 不是重装前的，则改成静态，使用之前的 IP\n# 只比较 IP，不比较掩码/网关，因为\n# 1. 假设掩码/网关导致无法上网，后面也会检测到并改成静态\n# 2. openSUSE wicked dhcpv6 是 64 位掩码，aws lightsail 模板上的也是，而其它 dhcpv6 软件都是 128 位掩码\nif $dhcpv4 && [ -n \"$ipv4_addr\" ] && [ -n \"$ipv4_gateway\" ] &&\n    ! [ \"$(echo \"$ipv4_addr\" | cut -d/ -f1)\" = \"$(get_first_ipv4_addr | cut -d/ -f1)\" ]; then\n    echo \"IPv4 address obtained from DHCP is different from old system.\"\n    should_disable_dhcpv4=true\n    flush_ipv4_config\nfi\nif $dhcpv6_or_slaac && [ -n \"$ipv6_addr\" ] && [ -n \"$ipv6_gateway\" ] &&\n    ! [ \"$(echo \"$ipv6_addr\" | cut -d/ -f1)\" = \"$(get_first_ipv6_addr | cut -d/ -f1)\" ]; then\n    echo \"IPv6 address obtained from SLAAC/DHCPv6 is different from old system.\"\n    should_disable_accept_ra=true\n    should_disable_autoconf=true\n    flush_ipv6_config\nfi\n\n# 设置静态地址，或者设置 debian 9 udhcpc 无法设置的网关\nadd_missing_ipv4_config\nadd_missing_ipv6_config\n\n# 检查 ipv4/ipv6 是否连接联网\nipv4_has_internet=false\nipv6_has_internet=false\ntest_internet\n\n# 如果无法上网，并且自动获取的 掩码/网关 不是重装前的，则改成静态\n# ip_addr 包括 IP/掩码，所以可以用来判断掩码是否不同\n# IP 不同的情况在前面已经改成静态了\nif ! $ipv4_has_internet &&\n    $dhcpv4 && [ -n \"$ipv4_addr\" ] && [ -n \"$ipv4_gateway\" ] &&\n    ! { [ \"$ipv4_addr\" = \"$(get_first_ipv4_addr)\" ] && [ \"$ipv4_gateway\" = \"$(get_first_ipv4_gateway)\" ]; }; then\n    echo \"IPv4 netmask/gateway obtained from DHCP is different from old system.\"\n    should_disable_dhcpv4=true\n    flush_ipv4_config\n    add_missing_ipv4_config\n    test_internet\nfi\n# 有可能是静态 IPv6 但能从 RA 获取到网关，因此加上 || $ra_has_gateway\nif ! $ipv6_has_internet &&\n    { $dhcpv6_or_slaac || $ra_has_gateway; } &&\n    [ -n \"$ipv6_addr\" ] && [ -n \"$ipv6_gateway\" ] &&\n    ! { [ \"$ipv6_addr\" = \"$(get_first_ipv6_addr)\" ] && [ \"$ipv6_gateway\" = \"$(get_first_ipv6_gateway)\" ]; }; then\n    echo \"IPv6 netmask/gateway obtained from SLAAC/DHCPv6 is different from old system.\"\n    should_disable_accept_ra=true\n    should_disable_autoconf=true\n    flush_ipv6_config\n    add_missing_ipv6_config\n    test_internet\nfi\n\n# 要删除不联网协议的ip，因为\n# 1 甲骨文云管理面板添加ipv6地址然后取消\n#   依然会分配ipv6地址，但ipv6没网络\n#   此时alpine只会用ipv6下载apk，而不用会ipv4下载\n# 2 有ipv4地址但没有ipv4网关的情况(vultr $2.5 ipv6 only)，aria2会用ipv4下载\n\n# 假设 ipv4 ipv6 在不同网卡，ipv4 能上网但 ipv6 不能上网，这时也要删除 ipv6\n# 不能用 ipv4_has_internet && ! ipv6_has_internet 判断，因为它判断的是同一个网卡\nif ! $ipv4_has_internet; then\n    if $dhcpv4; then\n        should_disable_dhcpv4=true\n    fi\n    flush_ipv4_config\nfi\nif ! $ipv6_has_internet; then\n    # 防止删除 IPv6 后再次通过 SLAAC 获得\n    # 不用判断 || $ra_has_gateway ，因为没有 IPv6 地址但有 IPv6 网关时，不会出现下载问题\n    if $dhcpv6_or_slaac; then\n        should_disable_accept_ra=true\n        should_disable_autoconf=true\n    fi\n    flush_ipv6_config\nfi\n\n# 如果联网了，但没获取到默认 DNS，则添加我们的 DNS\n\n# 有一种情况是，多网卡，且能上网的网卡先完成了这个脚本，不能上网的网卡后完成\n# 无法上网的网卡通过 flush_ipv4_config 删除了不能上网的 IP 和 dns\n# （原计划是删除无法上网的网卡 dhcp4 获取的 dns，但实际上无法区分）\n# 因此这里直接添加 dns，不判断是否联网\nif ! is_have_ipv4_dns; then\n    echo \"nameserver $ipv4_dns1\" >>/etc/resolv.conf\n    echo \"nameserver $ipv4_dns2\" >>/etc/resolv.conf\nfi\nif ! is_have_ipv6_dns; then\n    echo \"nameserver $ipv6_dns1\" >>/etc/resolv.conf\n    echo \"nameserver $ipv6_dns2\" >>/etc/resolv.conf\nfi\n\n# 传参给 trans.start\nnetconf=\"/dev/netconf/$ethx\"\nmkdir -p \"$netconf\"\n$dhcpv4 && echo 1 >\"$netconf/dhcpv4\" || echo 0 >\"$netconf/dhcpv4\"\n$dhcpv6_or_slaac && echo 1 >\"$netconf/dhcpv6_or_slaac\" || echo 0 >\"$netconf/dhcpv6_or_slaac\"\n$should_disable_dhcpv4 && echo 1 >\"$netconf/should_disable_dhcpv4\" || echo 0 >\"$netconf/should_disable_dhcpv4\"\n$should_disable_accept_ra && echo 1 >\"$netconf/should_disable_accept_ra\" || echo 0 >\"$netconf/should_disable_accept_ra\"\n$should_disable_autoconf && echo 1 >\"$netconf/should_disable_autoconf\" || echo 0 >\"$netconf/should_disable_autoconf\"\n$is_in_china && echo 1 >\"$netconf/is_in_china\" || echo 0 >\"$netconf/is_in_china\"\necho \"$ethx\" >\"$netconf/ethx\"\necho \"$mac_addr\" >\"$netconf/mac_addr\"\necho \"$ipv4_addr\" >\"$netconf/ipv4_addr\"\necho \"$ipv4_gateway\" >\"$netconf/ipv4_gateway\"\necho \"$ipv6_addr\" >\"$netconf/ipv6_addr\"\necho \"$ipv6_gateway\" >\"$netconf/ipv6_gateway\"\necho \"$ipv6_extra_addrs\" >\"$netconf/ipv6_extra_addrs\"\n$ipv4_has_internet && echo 1 >\"$netconf/ipv4_has_internet\" || echo 0 >\"$netconf/ipv4_has_internet\"\n$ipv6_has_internet && echo 1 >\"$netconf/ipv6_has_internet\" || echo 0 >\"$netconf/ipv6_has_internet\"\n"
  },
  {
    "path": "logviewer-nginx.conf",
    "content": "server {\n    listen @WEB_PORT@;\n    listen [::]:@WEB_PORT@;\n    root /;\n\n    gzip on;\n    gzip_types text/plain;\n\n\n    location = / {\n        try_files /logviewer.html 404;\n    }\n\n    location = /reinstall.log {\n        types {\n            text/plain log;\n        }\n\n        try_files $uri 404;\n    }\n\n    location / {\n        return 404;\n    }\n}"
  },
  {
    "path": "logviewer.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n    <title>Reinstall Logs</title>\n    <style>\n        body {\n            margin: 0;\n            padding: 0;\n            overflow: hidden;\n        }\n\n        #log-container {\n            height: calc(100vh);\n            margin: 0;\n            padding: 8px;\n            overflow-y: scroll;\n        }\n\n        #scroll-to-bottom {\n            position: fixed;\n            bottom: 24px;\n            right: 24px;\n            background-color: #0099FF;\n            color: #fff;\n            border: none;\n            cursor: pointer;\n            display: none;\n            width: 48px;\n            height: 48px;\n            border-radius: 50%;\n        }\n\n        #scroll-to-bottom:hover {\n            background-color: #00CCFF;\n        }\n\n        #scroll-to-bottom svg {\n            fill: #fff;\n        }\n\n        .done {\n            background-color: #cfc;\n        }\n\n        .error {\n            background-color: #fcc;\n        }\n    </style>\n</head>\n\n<body>\n    <pre id=\"log-container\"></pre>\n    <button id=\"scroll-to-bottom\">\n        <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\">\n            <path d=\"M5 10l7 7 7-7H5z\" />\n        </svg>\n    </button>\n\n    <script\n        src=\"https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/reconnecting-websocket/1.0.0/reconnecting-websocket.min.js\"\n        type=\"application/javascript\"></script>\n\n    <script>\n        const logContainer = document.getElementById('log-container');\n        const scrollToBottomButton = document.getElementById('scroll-to-bottom');\n        let shouldScrollToBottom = true;\n\n        // 缓冲区相关\n        let messageBuffer = [];\n        let flushScheduled = false;\n        const BUFFER_FLUSH_INTERVAL = 100; // 毫秒\n        const BUFFER_MAX_SIZE = 50; // 最大缓冲消息数\n\n        scrollToBottomButton.addEventListener('click', () => {\n            logContainer.scrollTop = logContainer.scrollHeight;\n        });\n\n        logContainer.addEventListener('scroll', () => {\n            const isAtBottom = Math.ceil(logContainer.scrollTop + logContainer.clientHeight) >= logContainer.scrollHeight;\n            if (isAtBottom) {\n                scrollToBottomButton.style.display = 'none';\n            } else {\n                scrollToBottomButton.style.display = 'block';\n            }\n\n            shouldScrollToBottom = isAtBottom;\n        });\n\n        // 刷新缓冲区到 DOM\n        function flushBuffer() {\n            if (messageBuffer.length === 0) {\n                flushScheduled = false;\n                return;\n            }\n\n            // 批量更新文本内容\n            const batchText = messageBuffer.join('\\n');\n            logContainer.textContent += '\\n' + batchText;\n\n            // 检查状态变化（优先级：error > done > start）\n            if (batchText.includes('***** ERROR *****')) {\n                document.body.className = 'error';\n            } else if (batchText.includes('***** DONE *****')) {\n                document.body.className = 'done';\n            } else if (batchText.includes('***** START TRANS *****')) {\n                document.body.className = '';\n            }\n\n            // 自动滚动\n            if (shouldScrollToBottom) {\n                logContainer.scrollTop = logContainer.scrollHeight;\n            }\n\n            // 清空缓冲区\n            messageBuffer = [];\n            flushScheduled = false;\n        }\n\n        // 调度刷新\n        function scheduleFlush() {\n            if (!flushScheduled) {\n                flushScheduled = true;\n                requestAnimationFrame(() => {\n                    setTimeout(flushBuffer, BUFFER_FLUSH_INTERVAL);\n                });\n            }\n        }\n\n        // 添加消息到缓冲区\n        function bufferMessage(message) {\n            messageBuffer.push(message);\n\n            // 如果缓冲区满了，立即刷新\n            if (messageBuffer.length >= BUFFER_MAX_SIZE) {\n                if (flushScheduled) {\n                    // 取消之前的调度，立即刷新\n                    flushScheduled = false;\n                }\n                flushBuffer();\n            } else {\n                scheduleFlush();\n            }\n        }\n\n        var ws = new ReconnectingWebSocket('ws://' + location.host + '/');\n        ws.onopen = function () {\n            bufferMessage('WebSocket Connected.');\n        };\n        ws.onclose = function () {\n            bufferMessage('WebSocket Disconnected.');\n        };\n        ws.onmessage = function (event) {\n            bufferMessage(event.data);\n        };\n    </script>\n</body>\n\n</html>\n"
  },
  {
    "path": "redhat.cfg",
    "content": "# shellcheck disable=SC2148\n# 设置\nkeyboard --vckeymap=us --xlayouts='us'\nlang en_US.UTF-8\ntimezone Asia/Shanghai --utc\nrootpw --plaintext 123@@@\ntext\nreboot\n%include /tmp/include-url-command\n\n# 分区\n%include /tmp/include-disk-only-use\n%include /tmp/include-bootloader\nclearpart --all --initlabel\nreqpart # 如果需要，自动创建 efi 或 biosboot 分区\npart / --fstype=xfs --grow\n\n# 软件\n%packages --ignoremissing # el9 minimal.iso fedora Server repo/iso 没有 tuned\n@^Minimal Install\n%include /tmp/include-packages-for-resize\n%include /tmp/exclude-packages-for-vm\n%end\n\n# 禁用防火墙\n# firewall --disabled\n\n# 禁用 selinux\nselinux --disabled\n\n# 禁用 kdump\n%addon com_redhat_kdump --disable\n%end\n\n##############################################\n%pre\ndistro=$(awk -F: '{ print $3 }' </etc/system-release-cpe)\nreleasever=$(awk -F: '{ print $5 }' </etc/system-release-cpe)\n\n# 重新整理 extra，grub把两侧的引号吃掉了，eval出错，要重新添加引号\n# 提取 extra_confhome extra_mirrorlist extra_main_disk\nprefix=extra\nfor var in $(grep -o \"\\b${prefix}_[^ ]*\" /proc/cmdline | xargs); do\n    eval \"$(echo \"$var\" | sed -E \"s/${prefix}_([^=]*)=(.*)/\\1='\\2'/\")\"\ndone\n\n# centos7 证书链未更新，需要 --no-check-certificate\n\n# 只使用主硬盘\ninclude=/tmp/include-disk-only-use\nxda=$(wget --no-check-certificate --tries=5 \"$confhome/get-xda.sh\" -O- | sh -s)\necho \"ignoredisk --only-use=$xda\" >$include\n\n# 设置 tty\ninclude=/tmp/include-bootloader\n# shellcheck disable=SC2154\nconsole_cmdline=$(wget --no-check-certificate --tries=5 \"$confhome/ttys.sh\" -O- | sh -s console=)\necho \"bootloader --append=\\\"$console_cmdline\\\"\" >$include\n\n# 有 installer 分区，表示用了两步安装\ninclude=/tmp/include-packages-for-resize\ntouch $include\nif [ -e /dev/disk/by-label/installer ]; then\n    # 1g内存下，安装器默认开启了zram ，但安装f38还是不够内存\n    # 具体表现为不断重启安装界面，所以还要开启swap\n    ram_size=$(lsmem -b 2>/dev/null | grep 'Total online memory:' | awk '{ print $NF/1024/1024 }')\n    if [ -z \"$ram_size\" ] || [ \"$ram_size\" -le 1024 ]; then\n        mount /dev/disk/by-label/installer /run/install/repo -o remount,rw\n        swapfile=/run/install/repo/swapfile\n        if command -v fallocate; then\n            fallocate -l 1G $swapfile\n        else\n            dd if=/dev/zero of=$swapfile bs=1M count=1024\n        fi\n        chmod 0600 $swapfile\n        mkswap $swapfile\n        swapon $swapfile\n    fi\n\n    # feroda 默认不包含 cronie\n    echo cronie >>$include\n\n    # el7 的parted不支持在线扩容，要用 growpart 和 gdisk 处理 gpt 分区\n    if [ \"$releasever\" = \"7\" ]; then\n        echo cloud-utils-growpart >>$include\n        echo gdisk >>$include\n    fi\nfi\n\n# 排除虚拟机用不上的组件\ninclude=/tmp/exclude-packages-for-vm\ntouch $include\nif systemd-detect-virt -v; then\n    cat <<EOF >$include\n        # 不删除usb相关的包 因为甲骨文云有usb设备 作用未知\n        # -usb_modeswitch\n        # -usbutils\n\n        # 无线\n        -iw\n        -crda\n        -rfkill\n        -iwl*-firmware\n\n        # 其他\n        -irqbalance # 多核+直通设备可能有用？\n        -microcode_ctl\n        -smartmontools\n\n        # 各种固件\n        -aic94xx-firmware\n        -alsa-firmware\n        -ivtv-firmware\n        # -linux-firmware # 去除后安装centos 8会报错\n\n        # fedora 特有固件\n        -amd-gpu-firmware\n        -atheros-firmware\n        -brcmfmac-firmware\n        -intel-gpu-firmware\n        -mt7xxx-firmware\n        -nvidia-gpu-firmware\n        -realtek-firmware\nEOF\nfi\n\n# 设置安装源\ninclude=/tmp/include-url-command\n# shellcheck disable=SC2154\nif [ \"$localtest\" = 1 ]; then\n    echo \"url --url=$confhome/$releasever/\" >$include\n    # echo cdrom >$include\nelse\n    echo \"url --mirrorlist=$mirrorlist\" >$include\n    # 对于el7/fedora, 添加了 updates repo 才会安装最新的包\n    if [ \"$releasever\" = \"7\" ] || [ \"$distro\" = \"fedoraproject\" ]; then\n        echo \"repo --name=updates\" >>$include\n    fi\nfi\n%end\n\n##############################################\n%post\n# el9/fedora的sshd默认不允许root密码登录，需手动开启\n# rootpw --allow-ssh 9.1 以上才支持\ndistro=$(awk -F: '{ print $3 }' </etc/system-release-cpe)\nreleasever=$(awk -F: '{ print $5 }' </etc/system-release-cpe)\nif [ \"$releasever\" = \"9\" ] || [ \"$distro\" = \"fedoraproject\" ]; then\n    echo \"PermitRootLogin yes\" >/etc/ssh/sshd_config.d/01-permitrootlogin.conf\nfi\n\n# 分步安装的系统，要将最后一个分区（installer）合并到系统分区\nif [ -e /dev/disk/by-label/installer ]; then\n    # 提取 extra_localtest extra_confhome extra_mirrorlist\n    prefix=extra\n    for var in $(grep -o \"\\b${prefix}_[^ ]*\" /proc/cmdline | xargs); do\n        eval \"$(echo \"$var\" | sed -E \"s/${prefix}_([^=]*)=(.*)/\\1='\\2'/\")\"\n    done\n\n    cd /\n    curl -O \"$confhome/resize.sh\"\n    echo '@reboot root bash /resize.sh' >/etc/cron.d/resize\nfi\n%end\n"
  },
  {
    "path": "reinstall.bat",
    "content": "@echo off\r\nmode con cp select=437 >nul\r\nsetlocal EnableDelayedExpansion\r\n\r\nset confhome=https://raw.githubusercontent.com/bin456789/reinstall/main\r\nset confhome_cn=https://cnb.cool/bin456789/reinstall/-/git/raw/main\r\nrem set confhome_cn=https://www.ghproxy.cc/https://raw.githubusercontent.com/bin456789/reinstall/main\r\n\r\nset pkgs=curl,cpio,p7zip,dos2unix,jq,xz,gzip,zstd,openssl,bind-utils,libiconv,binutils\r\nset cmds=curl,cpio,p7zip,dos2unix,jq,xz,gzip,zstd,openssl,nslookup,iconv,ar\r\n\r\nrem 65001 代码页会乱码\r\n\r\nrem 不要用 :: 注释\r\nrem 否则可能会出现 系统找不到指定的驱动器\r\n\r\nrem Windows 7 SP1 winhttp 默认不支持 tls 1.2\r\nrem 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\r\nrem 有些系统根证书没更新\r\nrem 所以不要用https\r\nrem 进入脚本目录\r\ncd /d %~dp0\r\n\r\nrem 检查是否有管理员权限\r\nfltmc >nul 2>&1\r\nif errorlevel 1 (\r\n    echo Please run as administrator^^!\r\n    exit /b\r\n)\r\n\r\nrem 有时 %tmp% 带会话 id，且文件夹不存在\r\nrem https://learn.microsoft.com/troubleshoot/windows-server/shell-experience/temp-folder-with-logon-session-id-deleted\r\nrem if not exist %tmp% (\r\nrem     md %tmp%\r\nrem )\r\n\r\nrem 下载 geoip\r\nif not exist geoip (\r\n    rem www.cloudflare.com/dash.cloudflare.com 国内访问的是美国服务器，而且部分地区被墙\r\n    call :download http://www.qualcomm.cn/cdn-cgi/trace %~dp0geoip || goto :download_failed\r\n)\r\n\r\nrem 判断是否有 loc=\r\nfindstr /c:\"loc=\" geoip >nul\r\nif errorlevel 1 (\r\n    echo Invalid geoip file\r\n    del geoip\r\n    exit /b 1\r\n)\r\n\r\nrem 检查是否国内\r\nfindstr /c:\"loc=CN\" geoip >nul\r\nif not errorlevel 1 (\r\n    rem mirrors.tuna.tsinghua.edu.cn 会强制跳转 https\r\n    set mirror=http://mirror.nju.edu.cn\r\n    if defined confhome_cn (\r\n        set confhome=!confhome_cn!\r\n    ) else if defined github_proxy (\r\n        echo !confhome! | findstr /c:\"://raw.githubusercontent.com/\" >nul\r\n        if not errorlevel 1 (\r\n            set confhome=!confhome:http://=https://!\r\n            set confhome=!confhome:https://raw.githubusercontent.com=%github_proxy%!\r\n        )\r\n    )\r\n) else (\r\n    rem 服务器在美国 equinix 机房，不是 cdn\r\n    set mirror=http://mirrors.kernel.org\r\n)\r\n\r\ncall :check_cygwin_installed || (\r\n    rem win10 arm 支持运行 x86 软件\r\n    rem win11 arm 支持运行 x86 和 x86_64 软件\r\n\r\n    rem windows 11 24h2 没有 wmic\r\n    rem wmic os get osarchitecture 显示中文，即使设置了 mode con cp select=437\r\n    rem wmic ComputerSystem get SystemType 显示英文\r\n    rem for /f \"tokens=*\" %%a in ('wmic ComputerSystem get SystemType ^| find /i \"based\"') do (\r\n    rem     set \"SystemType=%%a\"\r\n    rem )\r\n\r\n    rem 有的系统精简了 powershell\r\n    rem for /f \"delims=\" %%a in ('powershell -NoLogo -NoProfile -NonInteractive -Command \"(Get-WmiObject win32_computersystem).SystemType\"') do (\r\n    rem     set \"SystemType=%%a\"\r\n    rem )\r\n\r\n    rem SystemArch\r\n    for /f \"tokens=3\" %%a in ('reg query \"HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\" /v PROCESSOR_ARCHITECTURE') do (\r\n        set SystemArch=%%a\r\n    )\r\n\r\n    rem 也可以用 PROCESSOR_ARCHITEW6432 和 PROCESSOR_ARCHITECTURE 判断\r\n    rem ARM64 win11  PROCESSOR_ARCHITEW6432   PROCESSOR_ARCHITECTURE\r\n    rem 原生cmd          未定义                      ARM64\r\n    rem 32位cmd          ARM64                       x86\r\n\r\n    rem if defined PROCESSOR_ARCHITEW6432 (\r\n    rem     set \"SystemArch=%PROCESSOR_ARCHITEW6432%\"\r\n    rem ) else (\r\n    rem     set \"SystemArch=%PROCESSOR_ARCHITECTURE%\"\r\n    rem )\r\n\r\n    rem BuildNumber\r\n    for /f \"tokens=3\" %%a in ('reg query \"HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\" /v CurrentBuildNumber') do (\r\n        set /a BuildNumber=%%a\r\n    )\r\n\r\n    set CygwinEOL=1\r\n\r\n    echo !SystemArch! | find \"ARM\" > nul\r\n    if not errorlevel 1 (\r\n        if !BuildNumber! GEQ 22000 (\r\n            set CygwinEOL=0\r\n        )\r\n    ) else (\r\n        echo !SystemArch! | find \"AMD64\" > nul\r\n        if not errorlevel 1 (\r\n            if !BuildNumber! GEQ 9600 (\r\n                set CygwinEOL=0\r\n            )\r\n        )\r\n    )\r\n\r\n    rem win7/8 cygwin 已 EOL，不能用最新 cygwin 源，而要用 Cygwin Time Machine 源\r\n    rem 但 Cygwin Time Machine 没有国内源\r\n    rem 为了保证国内下载速度, cygwin EOL 统一使用 cygwin-archive x86 源\r\n    if !CygwinEOL! == 1 (\r\n        set CygwinArch=x86\r\n        set dir=/sourceware/cygwin-archive/20221123\r\n    ) else (\r\n        set CygwinArch=x86_64\r\n        set dir=/sourceware/cygwin\r\n    )\r\n\r\n    rem daocloud 加速有 90 天缓存，且不支持 IPv6\r\n    rem https://github.com/DaoCloud/public-binary-files-mirror\r\n    rem 无法用查询字符串强制刷新缓存\r\n    rem https://files.m.daocloud.io/www.cloudflare.com/cdn-cgi/trace?a=1\r\n    rem https://files.m.daocloud.io/www.cloudflare.com/cdn-cgi/trace?b=2\r\n    rem 也就无法用 https://www.cygwin.com/setup-x86_64.exe?xxx=20250101 强制每天刷新缓存\r\n\r\n    rem 下载 Cygwin\r\n    if not exist setup-!CygwinArch!.exe (\r\n        call :download http://www.cygwin.com/setup-!CygwinArch!.exe %~dp0setup-!CygwinArch!.exe || goto :download_failed\r\n    )\r\n\r\n    rem 少于 1M 视为无效\r\n    rem 有的 IP 被官网拉黑，无法下载 exe，下载得到 html\r\n    for %%A in (setup-!CygwinArch!.exe) do if %%~zA LSS 1048576 (\r\n        echo Invalid Cgywin installer\r\n        del setup-!CygwinArch!.exe\r\n        exit /b 1\r\n    )\r\n\r\n    rem 安装 Cygwin\r\n    set site=!mirror!!dir!\r\n    start /wait setup-!CygwinArch!.exe ^\r\n        --allow-unsupported-windows ^\r\n        --quiet-mode ^\r\n        --only-site ^\r\n        --site !site! ^\r\n        --root %SystemDrive%\\cygwin ^\r\n        --local-package-dir %~dp0cygwin-local-package-dir ^\r\n        --packages %pkgs%\r\n\r\n    rem 检查 Cygwin 是否成功安装\r\n    if errorlevel 1 goto :install_cygwin_failed\r\n    call :check_cygwin_installed || goto :install_cygwin_failed\r\n)\r\n\r\nrem 在c盘根目录下执行 cygpath -ua . 会得到 /cygdrive/c，因此末尾要有 /\r\nfor /f %%a in ('%SystemDrive%\\cygwin\\bin\\cygpath -ua ./') do set thisdir=%%a\r\n\r\nrem 下载 reinstall.sh\r\nif not exist reinstall.sh (\r\n    call :download_with_curl %confhome%/reinstall.sh %thisdir%reinstall.sh || goto :download_failed\r\n    call :chmod a+x %thisdir%reinstall.sh\r\n)\r\n\r\nrem %* 无法处理 --iso https://x.com/?yyy=123\r\nrem 为每个参数添加引号，使参数正确传递到 bash\r\nrem for %%a in (%*) do (\r\nrem     set \"param=!param! \"%%~a\"\"\r\nrem )\r\n\r\nrem 转成 unix 格式，避免用户用 windows 记事本编辑后换行符不对\r\n%SystemDrive%\\cygwin\\bin\\dos2unix -q '%thisdir%reinstall.sh'\r\n\r\nrem 用 bash 运行\r\nrem %SystemDrive%\\cygwin\\bin\\bash -l %thisdir%reinstall.sh %* 运行后会清屏\r\nrem 因此不能用 -l\r\nrem 这就需要在 reinstall.sh 里运行 source /etc/profile\r\nrem 或者添加 export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH\r\n%SystemDrive%\\cygwin\\bin\\bash %thisdir%reinstall.sh %*\r\nexit /b\r\n\r\nrem bits 要求有 Content-Length 才能下载\r\nrem cloudflare 的 cdn-cgi/trace 没有 Content-Length\r\nrem 据说如果网络设为“按流量计费” bits 也无法下载\r\nrem https://learn.microsoft.com/en-us/windows/win32/bits/http-requirements-for-bits-downloads\r\nrem bitsadmin /transfer \"%~3\" /priority foreground %~1 %~2\r\n\r\n:download\r\nrem certutil 会被 windows Defender 报毒\r\nrem windows server 2019 要用第二条 certutil 命令\r\necho Downloading: %~1 %~2\r\ndel /q \"%~2\" 2>nul\r\nif exist \"%~2\" (echo Cannot delete %~2 & exit /b 1)\r\n\r\ncertutil -urlcache -f -split \"%~1\" \"%~2\" >nul\r\nif not errorlevel 1 if exist \"%~2\" exit /b 0\r\n\r\ncertutil -urlcache -split \"%~1\" \"%~2\" >nul\r\nif not errorlevel 1 if exist \"%~2\" exit /b 0\r\n\r\nrem 下载失败时删除文件，防止下载了一部分导致下次运行时跳过了下载\r\ndel /q \"%~2\" 2>nul\r\nexit /b 1\r\n\r\n:download_with_curl\r\nrem 加 --insecure 防止以下错误\r\nrem curl: (77) error setting certificate verify locations:\r\nrem   CAfile: /etc/ssl/certs/ca-certificates.crt\r\nrem   CApath: none\r\necho Download: %~1 %~2\r\n%SystemDrive%\\cygwin\\bin\\curl -L --insecure \"%~1\" -o \"%~2\"\r\nexit /b\r\n\r\n:chmod\r\n%SystemDrive%\\cygwin\\bin\\chmod \"%~1\" \"%~2\"\r\nexit /b\r\n\r\n:download_failed\r\necho Download failed.\r\nexit /b 1\r\n\r\n:install_cygwin_failed\r\necho Failed to install Cygwin.\r\nexit /b 1\r\n\r\n:check_cygwin_installed\r\nset \"cmds_space=%cmds:,= %\"\r\nfor %%c in (%cmds_space%) do (\r\n    if not exist \"%SystemDrive%\\cygwin\\bin\\%%c\" if not exist \"%SystemDrive%\\cygwin\\bin\\%%c.exe\" (\r\n        exit /b 1\r\n    )\r\n)\r\nexit /b 0\r\n"
  },
  {
    "path": "reinstall.sh",
    "content": "#!/usr/bin/env sh\n# shellcheck shell=bash\n# shellcheck disable=SC2086\n\n# nixos 默认的配置不会生成 /bin/bash，因此需要用 /usr/bin/env\n# alpine 默认没有 bash，因此 shebang 用 sh，再 exec 切换到 bash\n\nset -eE\nconfhome=https://raw.githubusercontent.com/bin456789/reinstall/main\nconfhome_cn=https://cnb.cool/bin456789/reinstall/-/git/raw/main\n# confhome_cn=https://www.ghproxy.cc/https://raw.githubusercontent.com/bin456789/reinstall/main\n\n# 用于判断 reinstall.sh 和 trans.sh 是否兼容\nSCRIPT_VERSION=4BACD833-A585-23BA-6CBB-9AA4E08E0004\n\n# 记录要用到的 windows 程序，运行时输出删除 \\r\nWINDOWS_EXES='cmd powershell wmic reg diskpart netsh bcdedit mountvol'\n\nBOOT_ENTEY_START_MARK='### BEGIN reinstall.sh ###'\nBOOT_ENTEY_END_MARK='### END reinstall.sh ###'\n\n# 临时目录\n# 不用 /tmp，因为 /tmp 挂载在内存的话，可能不够空间\ntmp=/reinstall-tmp\n\n# 强制 linux 程序输出英文，防止 grep 不到想要的内容\n# https://www.gnu.org/software/gettext/manual/html_node/The-LANGUAGE-variable.html\nexport LC_ALL=C\n\n# 处理部分用户用 su 切换成 root 导致环境变量没 sbin 目录\n# 也能处理 cygwin bash 没有添加 -l 运行 reinstall.sh\n# 不要漏了最后的 $PATH，否则会找不到 windows 系统程序例如 diskpart\nexport PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH\n\n# 如果不是 bash 的话，继续执行会有语法错误，因此在这里判断是否 bash\nif [ -z \"$BASH\" ]; then\n    if ! command -v bash >/dev/null; then\n        if [ -f /etc/alpine-release ]; then\n            if ! apk add bash; then\n                echo \"Error while install bash.\" >&2\n                exit 1\n            fi\n        else\n            echo \"Please run this script with bash.\" >&2\n            exit 1\n        fi\n    fi\n    exec bash \"$0\" \"$@\"\nfi\n\n# 好像跟 trap SIGINT 有冲突\n# 记录日志，过滤含有 password 的行\n# exec > >(tee >(grep -iv password >>/reinstall.log)) 2>&1\nTHIS_SCRIPT=$(readlink -f \"$0\")\ntrap 'trap_err $LINENO $?' ERR\n\ntrap_err() {\n    line_no=$1\n    ret_no=$2\n\n    error \"Line $line_no return $ret_no\"\n    sed -n \"$line_no\"p \"$THIS_SCRIPT\"\n}\n\nis_in_windows() {\n    [ \"$(uname -o)\" = Cygwin ] || [ \"$(uname -o)\" = Msys ]\n}\n\nif is_in_windows; then\n    reinstall_____='.\\reinstall.bat'\nelse\n    reinstall_____='sh reinstall.sh'\nfi\n\nusage_and_exit() {\n    cat <<EOF\nUsage: $reinstall_____ anolis      7|8|23\n                       opencloudos 8|9|23\n                       rocky       8|9|10\n                       oracle      8|9|10\n                       almalinux   8|9|10\n                       centos      9|10\n                       fnos        1\n                       nixos       25.11\n                       fedora      42|43\n                       debian      9|10|11|12|13\n                       alpine      3.20|3.21|3.22|3.23\n                       opensuse    15.6|16.0|tumbleweed\n                       openeuler   20.03|22.03|24.03|25.09\n                       ubuntu      16.04|18.04|20.04|22.04|24.04|25.10 [--minimal]\n                       kali\n                       arch\n                       gentoo\n                       aosc\n                       redhat      --img=\"http://access.cdn.redhat.com/xxx.qcow2\"\n                       dd          --img=\"http://xxx.com/yyy.zzz\" (raw image stores in raw/vhd/tar/gz/xz/zst)\n                       windows     --image-name=\"windows xxx yyy\" --lang=xx-yy\n                       windows     --image-name=\"windows xxx yyy\" --iso=\"http://xxx.com/xxx.iso\"\n                       netboot.xyz\n                       reset\n\n       Options:        For Linux/Windows:\n                       [--password    PASSWORD]\n                       [--ssh-key     KEY]\n                       [--ssh-port    PORT]\n                       [--web-port    PORT]\n                       [--frpc-config PATH]\n\n                       For Windows Only:\n                       [--allow-ping]\n                       [--rdp-port    PORT]\n                       [--add-driver  INF_OR_DIR]\n\nManual: https://github.com/bin456789/reinstall\n\nEOF\n    exit 1\n}\n\ninfo() {\n    local msg\n    if [ \"$1\" = false ]; then\n        shift\n        msg=$*\n    else\n        msg=\"***** $(to_upper <<<\"$*\") *****\"\n    fi\n    echo_color_text '\\e[32m' \"$msg\" >&2\n}\n\nwarn() {\n    local msg\n    if [ \"$1\" = false ]; then\n        shift\n        msg=$*\n    else\n        msg=\"Warning: $*\"\n    fi\n    echo_color_text '\\e[33m' \"$msg\" >&2\n}\n\nerror() {\n    echo_color_text '\\e[31m' \"***** ERROR *****\" >&2\n    echo_color_text '\\e[31m' \"$*\" >&2\n}\n\necho_color_text() {\n    color=\"$1\"\n    shift\n    plain=\"\\e[0m\"\n    echo -e \"$color$*$plain\"\n}\n\nerror_and_exit() {\n    error \"$@\"\n    exit 1\n}\n\nshow_dd_password_tips() {\n    warn false \"\nThis password is only used for SSH access to view logs during the installation.\nPassword of the image will NOT modify.\n\n密码仅用于安装过程中通过 SSH 查看日志。\n镜像的密码不会被修改。\n\"\n}\n\nshow_url_in_args() {\n    while [ $# -gt 0 ]; do\n        case \"$1\" in\n        [Hh][Tt][Tt][Pp][Ss]://* | [Hh][Tt][Tt][Pp]://* | [Mm][Aa][Gg][Nn][Ee][Tt]:*) echo \"$1\" ;;\n        esac\n        shift\n    done\n}\n\ncurl() {\n    is_have_cmd curl || install_pkg curl\n\n    # 显示 url\n    show_url_in_args \"$@\" >&2\n\n    # 添加 -f, --fail，不然 404 退出码也为0\n    # 32位 cygwin 已停止更新，证书可能有问题，先添加 --insecure\n    # centos 7 curl 不支持 --retry-connrefused --retry-all-errors\n    # 因此手动 retry\n    for i in $(seq 5); do\n        if command curl --insecure --connect-timeout 10 -f \"$@\"; then\n            return\n        else\n            ret=$?\n            # 403 404 错误，或者达到重试次数\n            if [ $ret -eq 22 ] || [ $i -eq 5 ]; then\n                return $ret\n            fi\n            sleep 1\n        fi\n    done\n}\n\nmask2cidr() {\n    local x=${1##*255.}\n    set -- 0^^^128^192^224^240^248^252^254^ $(((${#1} - ${#x}) * 2)) ${x%%.*}\n    x=${1%%\"$3\"*}\n    echo $(($2 + (${#x} / 4)))\n}\n\nis_in_china() {\n    [ \"$force_cn\" = 1 ] && return 0\n\n    if [ -z \"$_loc\" ]; then\n        # www.cloudflare.com/dash.cloudflare.com 国内访问的是美国服务器，而且部分地区被墙\n        # 没有ipv6 www.visa.cn\n        # 没有ipv6 www.bose.cn\n        # 没有ipv6 www.garmin.com.cn\n        # 备用 www.prologis.cn\n        # 备用 www.autodesk.com.cn\n        # 备用 www.keysight.com.cn\n        if ! _loc=$(curl -L http://www.qualcomm.cn/cdn-cgi/trace | grep '^loc=' | cut -d= -f2 | grep .); then\n            error_and_exit \"Can not get location.\"\n        fi\n        echo \"Location: $_loc\" >&2\n    fi\n    [ \"$_loc\" = CN ]\n}\n\nis_in_windows() {\n    [ \"$(uname -o)\" = Cygwin ] || [ \"$(uname -o)\" = Msys ]\n}\n\nis_in_alpine() {\n    [ -f /etc/alpine-release ]\n}\n\nis_use_cloud_image() {\n    [ -n \"$cloud_image\" ] && [ \"$cloud_image\" = 1 ]\n}\n\nis_force_use_installer() {\n    [ -n \"$installer\" ] && [ \"$installer\" = 1 ]\n}\n\nis_use_dd() {\n    [ \"$distro\" = dd ]\n}\n\nis_boot_in_separate_partition() {\n    mount | grep -q ' on /boot type '\n}\n\nis_os_in_btrfs() {\n    mount | grep -q ' on / type btrfs '\n}\n\nis_os_in_subvol() {\n    subvol=$(awk '($2==\"/\") { print $i }' /proc/mounts | grep -o 'subvol=[^ ]*' | cut -d= -f2)\n    [ \"$subvol\" != / ]\n}\n\nget_os_part() {\n    awk '($2==\"/\") { print $1 }' /proc/mounts\n}\n\numount_all() {\n    # windows defender 打开时，cygwin 运行 mount 很慢，但 cat /proc/mounts 很快\n    if mount_lists=$(mount | grep -w \"on $1\" | awk '{print $3}' | grep .); then\n        # alpine 没有 -R\n        if umount --help 2>&1 | grep -wq -- '-R'; then\n            umount -R \"$1\"\n        else\n            echo \"$mount_lists\" | tac | xargs -n1 umount\n        fi\n    fi\n}\n\ncp_to_btrfs_root() {\n    mount_dir=$tmp/reinstall-btrfs-root\n    if ! grep -q $mount_dir /proc/mounts; then\n        mkdir -p $mount_dir\n        mount \"$(get_os_part)\" $mount_dir -t btrfs -o subvol=/\n    fi\n    cp -rf \"$@\" \"$mount_dir\"\n}\n\nis_host_has_ipv4_and_ipv6() {\n    host=$1\n\n    install_pkg dig\n    # dig会显示cname结果，cname结果以.结尾，grep -v '\\.$' 用于去除 cname 结果\n    res=$(dig +short $host A $host AAAA | grep -v '\\.$')\n    # 有.表示有ipv4地址，有:表示有ipv6地址\n    grep -q \\. <<<$res && grep -q : <<<$res\n}\n\nis_netboot_xyz() {\n    [ \"$distro\" = netboot.xyz ]\n}\n\nis_alpine_live() {\n    [ \"$distro\" = alpine ] && [ \"$hold\" = 1 ]\n}\n\nis_have_initrd() {\n    ! is_netboot_xyz\n}\n\nis_use_firmware() {\n    # shellcheck disable=SC2154\n    [ \"$nextos_distro\" = debian ] && ! is_virt\n}\n\nis_digit() {\n    [[ \"$1\" =~ ^[0-9]+$ ]]\n}\n\nis_port_valid() {\n    is_digit \"$1\" && [ \"$1\" -ge 1 ] && [ \"$1\" -le 65535 ]\n}\n\nget_host_by_url() {\n    cut -d/ -f3 <<<$1\n}\n\nget_scheme_and_host_by_url() {\n    cut -d/ -f1-3 <<<$1\n}\n\nget_function() {\n    declare -f \"$1\"\n}\n\nget_function_content() {\n    declare -f \"$1\" | sed '1d;2d;$d'\n}\n\ninsert_into_file() {\n    local file=$1\n    local location=$2\n    local regex_to_find=$3\n    shift 3\n\n    if ! [ -f \"$file\" ]; then\n        error_and_exit \"File not found: $file\"\n    fi\n\n    # 默认 grep -E\n    if [ $# -eq 0 ]; then\n        set -- -E\n    fi\n\n    line_num=$(grep \"$@\" -n \"$regex_to_find\" \"$file\" | cut -d: -f1)\n\n    found_count=$(echo \"$line_num\" | wc -l)\n    if [ ! \"$found_count\" -eq 1 ]; then\n        return 1\n    fi\n\n    case \"$location\" in\n    before) line_num=$((line_num - 1)) ;;\n    after) ;;\n    *) return 1 ;;\n    esac\n\n    sed -i \"${line_num}r /dev/stdin\" \"$file\"\n}\n\ntest_url() {\n    test_url_real false \"$@\"\n}\n\ntest_url_grace() {\n    test_url_real true \"$@\"\n}\n\ntest_url_real() {\n    grace=$1\n    url=$2\n    expect_types=$3\n    var_to_eval=$4\n    info test url\n\n    failed() {\n        $grace && return 1\n        error_and_exit \"$@\"\n    }\n\n    tmp_file=$tmp/img-test\n\n    # TODO: 好像无法识别 nixos 官方源的跳转\n    # 有的服务器不支持 range，curl会下载整个文件\n    # 所以用 head 限制 1M\n    # 过滤 curl 23 错误（head 限制了大小）\n    # 也可用 ulimit -f 但好像 cygwin 不支持\n    # ${PIPESTATUS[n]} 表示第n个管道的返回值\n    echo $url\n    for i in $(seq 5 -1 0); do\n        if command curl --insecure --connect-timeout 10 -Lfr 0-1048575 \"$url\" \\\n            1> >(exec head -c 1048576 >$tmp_file) \\\n            2> >(exec grep -v 'curl: (23)' >&2); then\n            break\n        else\n            ret=$?\n            msg=\"$url not accessible\"\n            case $ret in\n            22)\n                # 403 404\n                # 这里的 failed 虽然返回 1，但是不会中断脚本，因此要手动 return\n                failed \"$msg\"\n                return \"$ret\"\n                ;;\n            23)\n                # 限制了空间\n                break\n                ;;\n            *)\n                # 其他错误\n                if [ $i -eq 0 ]; then\n                    failed \"$msg\"\n                    return \"$ret\"\n                fi\n                ;;\n            esac\n            sleep 1\n        fi\n    done\n\n    # 如果要检查文件类型\n    if [ -n \"$expect_types\" ]; then\n        install_pkg file\n        real_type=$(file_enhanced $tmp_file)\n        echo \"File type: $real_type\"\n\n        # debian 9 ubuntu 16.04-20.04 可能会将 iso 识别成 raw\n        for type in $expect_types $([ \"$expect_types\" = iso ] && echo raw); do\n            if [[ .\"$real_type\" = *.\"$type\" ]]; then\n                # 如果要设置变量\n                if [ -n \"$var_to_eval\" ]; then\n                    IFS=. read -r \"${var_to_eval?}\" \"${var_to_eval}_warp\" <<<\"$real_type\"\n                fi\n                return\n            fi\n        done\n\n        failed \"$url\nExpected type: $expect_types\nActually type: $real_type\"\n    fi\n}\n\nfix_file_type() {\n    # gzip的mime有很多种写法\n    # centos7中显示为 x-gzip，在其他系统中显示为 gzip，可能还有其他\n    # 所以不用mime判断\n    # https://www.digipres.org/formats/sources/tika/formats/#application/gzip\n\n    # centos 7 上的 file 显示 qcow2 的 mime 为 application/octet-stream\n    # file debian-12-genericcloud-amd64.qcow2\n    # debian-12-genericcloud-amd64.qcow2: QEMU QCOW Image (v3), 2147483648 bytes\n    # file --mime debian-12-genericcloud-amd64.qcow2\n    # debian-12-genericcloud-amd64.qcow2: application/octet-stream; charset=binary\n\n    # --extension 不靠谱\n    # file -b /reinstall-tmp/img-test --mime-type\n    # application/x-qemu-disk\n    # file -b /reinstall-tmp/img-test --extension\n    # ???\n\n    # 1. 删除,;#\n    # DOS/MBR boot sector; partition 1: ...\n    # gzip compressed data, was ...\n    # # ISO 9660 CD-ROM filesystem data... (有些 file 版本开头输出有井号)\n\n    # 2. 删除开头的空格\n\n    # 3. 删除无意义的单词 POSIX, Unicode, UTF-8, ASCII\n    # POSIX tar archive (GNU)\n    # Unicode text, UTF-8 text\n    # UTF-8 Unicode text, with very long lines\n    # ASCII text\n\n    # 4. 下面两种都是 raw\n    # DOS/MBR boot sector\n    # x86 boot sector; partition 1: ...\n    sed -E \\\n        -e 's/[,;#]//g' \\\n        -e 's/^[[:space:]]*//' \\\n        -e 's/(POSIX|Unicode|UTF-8|ASCII)//gi' \\\n        -e 's/^DOS\\/MBR boot sector/raw/i' \\\n        -e 's/^x86 boot sector/raw/i' \\\n        -e 's/^Zstandard/zstd/i' \\\n        -e 's/^UDF/iso/i' \\\n        -e 's/^Windows imaging \\(WIM\\) image/wim/i' |\n        awk '{print $1}' | to_lower\n}\n\n# 不用 file -z，因为\n# 1. file -z 只能看透一层\n# 2. alpine file -z 无法看透部分镜像（前1M），例如：\n# guajibao-win10-ent-ltsc-2021-x64-cn-efi.vhd.gz\n# guajibao-win7-sp1-ent-x64-cn-efi.vhd.gz\n# win7-ent-sp1-x64-cn-efi.vhd.gz\n# 还要注意 centos 7 没有 -Z 只有 -z\nfile_enhanced() {\n    file=$1\n\n    full_type=\n    while true; do\n        type=\"$(file -b $file | fix_file_type)\"\n        full_type=\"$type.$full_type\"\n        case \"$type\" in\n        xz | gzip | zstd)\n            install_pkg \"$type\"\n            $type -dc <\"$file\" | head -c 1048576 >\"$file.inside\"\n            mv -f \"$file.inside\" \"$file\"\n            ;;\n        tar)\n            install_pkg \"$type\"\n            # 隐藏 gzip: unexpected end of file 提醒\n            tar xf \"$file\" -O 2>/dev/null | head -c 1048576 >\"$file.inside\"\n            mv -f \"$file.inside\" \"$file\"\n            ;;\n        *)\n            break\n            ;;\n        esac\n    done\n    # shellcheck disable=SC2001\n    echo \"$full_type\" | sed 's/\\.$//'\n}\n\nadd_community_repo_for_alpine() {\n    local alpine_ver\n\n    # 先检查原来的repo是不是egde\n    if grep -q '^http.*/edge/main$' /etc/apk/repositories; then\n        alpine_ver=edge\n    else\n        alpine_ver=v$(cut -d. -f1,2 </etc/alpine-release)\n    fi\n\n    if ! grep -q \"^http.*/$alpine_ver/community$\" /etc/apk/repositories; then\n        mirror=$(grep '^http.*/main$' /etc/apk/repositories | sed 's,/[^/]*/main$,,' | head -1)\n        echo $mirror/$alpine_ver/community >>/etc/apk/repositories\n    fi\n}\n\nis_in_container() {\n    { is_have_cmd systemd-detect-virt && systemd-detect-virt -qc; } ||\n        [ -d /proc/vz ] ||\n        { [ -f /proc/1/environ ] && grep -q container=lxc /proc/1/environ; }\n}\n\n# 使用 | del_br ，但返回 del_br 之前返回值\nrun_with_del_cr() {\n    if false; then\n        # ash 不支持 PIPESTATUS[n]\n        res=$(\"$@\") && ret=0 || ret=$?\n        echo \"$res\" | del_cr\n        return $ret\n    else\n        \"$@\" | del_cr\n        return ${PIPESTATUS[0]}\n    fi\n}\n\nrun_with_del_cr_template() {\n    if get_function _$exe >/dev/null; then\n        run_with_del_cr _$exe \"$@\"\n    else\n        run_with_del_cr command $exe \"$@\"\n    fi\n}\n\nwmic() {\n    if is_have_cmd wmic; then\n        # 如果参数没有 GET，添加 GET，防止以下报错\n        # wmic memorychip /format:list\n        # 此级别的开关异常。\n        has_get=false\n        for i in \"$@\"; do\n            # 如果参数有 GET\n            if [ \"$(to_upper <<<\"$i\")\" = GET ]; then\n                has_get=true\n                break\n            fi\n        done\n\n        # 输出为 /format:list 格式\n        if $has_get; then\n            command wmic \"$@\" /format:list\n        else\n            command wmic \"$@\" get /format:list\n        fi\n        return\n    fi\n\n    # powershell wmi 默认参数\n    local namespace='root\\cimv2'\n    local class=\n    local filter=\n    local props=\n\n    # namespace\n    if [[ \"$(to_upper <<<\"$1\")\" = /NAMESPACE* ]]; then\n        # 删除引号，删除 \\\\\n        namespace=$(cut -d: -f2 <<<\"$1\" | sed -e \"s/[\\\"']//g\" -e 's/\\\\\\\\//g')\n        shift\n    fi\n\n    # class\n    if [[ \"$(to_upper <<<\"$1\")\" = PATH ]]; then\n        class=$2\n        shift 2\n    else\n        # wmic alias list brief\n        case \"$(to_lower <<<\"$1\")\" in\n        nicconfig) class=Win32_NetworkAdapterConfiguration ;;\n        memorychip) class=Win32_PhysicalMemory ;;\n        *) class=Win32_$1 ;;\n        esac\n        shift\n    fi\n\n    # filter\n    if [[ \"$(to_upper <<<\"$1\")\" = WHERE ]]; then\n        filter=$2\n        shift 2\n    fi\n\n    # props\n    if [[ \"$(to_upper <<<\"$1\")\" = GET ]]; then\n        props=$2\n        shift 2\n    fi\n\n    if ! [ -f \"$tmp/wmic.ps1\" ]; then\n        curl -Lo \"$tmp/wmic.ps1\" \"$confhome/wmic.ps1\"\n    fi\n\n    # shellcheck disable=SC2046\n    powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass \\\n        -File \"$(cygpath -w \"$tmp/wmic.ps1\")\" \\\n        -Namespace \"$namespace\" \\\n        -Class \"$class\" \\\n        $([ -n \"$filter\" ] && echo -Filter \"$filter\") \\\n        $([ -n \"$props\" ] && echo -Properties \"$props\")\n}\n\nis_virt() {\n    if [ -z \"$_is_virt\" ]; then\n        if is_in_windows; then\n            # https://github.com/systemd/systemd/blob/main/src/basic/virt.c\n            # https://sources.debian.org/src/hw-detect/1.159/hw-detect.finish-install.d/08hw-detect/\n            vmstr='VMware|Virtual|Virtualization|VirtualBox|VMW|Hyper-V|Bochs|QEMU|KVM|OpenStack|KubeVirt|innotek|Xen|Parallels|BHYVE'\n            for name in ComputerSystem BIOS BaseBoard; do\n                if wmic $name | grep -Eiw $vmstr; then\n                    _is_virt=true\n                    break\n                fi\n            done\n\n            # 用运行 windows ，肯定够内存运行 alpine lts netboot\n            # 何况还能停止 modloop\n\n            # 没有风扇和温度信息，大概是虚拟机\n            # 阿里云 倚天710 arm 有温度传感器\n            # ovh KS-LE-3 没有风扇和温度信息？\n            if false && [ -z \"$_is_virt\" ] &&\n                ! wmic /namespace:'\\\\root\\cimv2' PATH Win32_Fan 2>/dev/null | grep -q ^Name &&\n                ! wmic /namespace:'\\\\root\\wmi' PATH MSAcpi_ThermalZoneTemperature 2>/dev/null | grep -q ^Name; then\n                _is_virt=true\n            fi\n        else\n            # aws t4g debian 11\n            # systemd-detect-virt: 为 none，即使装了dmidecode\n            # virt-what: 未装 deidecode时结果为空，装了deidecode后结果为aws\n            # 所以综合两个命令的结果来判断\n            if is_have_cmd systemd-detect-virt && systemd-detect-virt -v; then\n                _is_virt=true\n            fi\n\n            if [ -z \"$_is_virt\" ]; then\n                # debian 安装 virt-what 不会自动安装 dmidecode，因此结果有误\n                install_pkg dmidecode virt-what\n                # virt-what 返回值始终是0，所以用是否有输出作为判断\n                if [ -n \"$(virt-what)\" ]; then\n                    _is_virt=true\n                fi\n            fi\n        fi\n\n        if [ -z \"$_is_virt\" ]; then\n            _is_virt=false\n        fi\n        echo \"VM: $_is_virt\"\n    fi\n    $_is_virt\n}\n\nis_absolute_path() {\n    # 检查路径是否以/开头\n    # 注意语法和 ash 不同\n    [[ \"$1\" = /* ]]\n}\n\nis_cpu_supports_x86_64_v3() {\n    # 用 ld.so/cpuid/coreinfo.exe 更准确\n    # centos 7 /usr/lib64/ld-linux-x86-64.so.2 没有 --help\n    # alpine gcompat /lib/ld-linux-x86-64.so.2 没有 --help\n\n    # https://en.wikipedia.org/wiki/X86-64#Microarchitecture_levels\n    # https://learn.microsoft.com/sysinternals/downloads/coreinfo\n\n    # abm = popcnt + lzcnt\n    # /proc/cpuinfo 不显示 lzcnt, 可用 abm 代替，但 cygwin 也不显示 abm\n    # /proc/cpuinfo 不显示 osxsave, 故用 xsave 代替\n\n    need_flags=\"avx avx2 bmi1 bmi2 f16c fma movbe xsave\"\n    had_flags=$(grep -m 1 ^flags /proc/cpuinfo | awk -F': ' '{print $2}')\n\n    for flag in $need_flags; do\n        if ! grep -qw $flag <<<\"$had_flags\"; then\n            return 1\n        fi\n    done\n}\n\nassert_cpu_supports_x86_64_v3() {\n    if ! is_cpu_supports_x86_64_v3; then\n        error_and_exit \"Could not install $distro $releasever because the CPU does not support x86-64-v3.\"\n    fi\n}\n\n# sr-latn-rs 到 sr-latn\nen_us() {\n    echo \"$lang\" | awk -F- '{print $1\"-\"$2}'\n\n    # zh-hk 可回落到 zh-tw\n    if [ \"$lang\" = zh-hk ]; then\n        echo zh-tw\n    fi\n}\n\n# fr-ca 到 ca\nus() {\n    # 葡萄牙准确对应 pp\n    if [ \"$lang\" = pt-pt ]; then\n        echo pp\n        return\n    fi\n    # 巴西准确对应 pt\n    if [ \"$lang\" = pt-br ]; then\n        echo pt\n        return\n    fi\n\n    echo \"$lang\" | awk -F- '{print $2}'\n\n    # hk 额外回落到 tw\n    if [ \"$lang\" = zh-hk ]; then\n        echo tw\n    fi\n}\n\n# fr-ca 到 fr-fr\nen_en() {\n    echo \"$lang\" | awk -F- '{print $1\"-\"$1}'\n\n    # en-gb 额外回落到 en-us\n    if [ \"$lang\" = en-gb ]; then\n        echo en-us\n    fi\n}\n\n# fr-ca 到 fr\nen() {\n    # 巴西/葡萄牙回落到葡萄牙语\n    if [ \"$lang\" = pt-br ] || [ \"$lang\" = pt-pt ]; then\n        echo \"pp\"\n        return\n    fi\n\n    echo \"$lang\" | awk -F- '{print $1}'\n}\n\nenglish() {\n    case \"$lang\" in\n    ar-sa) echo Arabic ;;\n    bg-bg) echo Bulgarian ;;\n    cs-cz) echo Czech ;;\n    da-dk) echo Danish ;;\n    de-de) echo German ;;\n    el-gr) echo Greek ;;\n    en-gb) echo Eng_Intl ;;\n    en-us) echo English ;;\n    es-es) echo Spanish ;;\n    es-mx) echo Spanish_Latam ;;\n    et-ee) echo Estonian ;;\n    fi-fi) echo Finnish ;;\n    fr-ca) echo FrenchCanadian ;;\n    fr-fr) echo French ;;\n    he-il) echo Hebrew ;;\n    hr-hr) echo Croatian ;;\n    hu-hu) echo Hungarian ;;\n    it-it) echo Italian ;;\n    ja-jp) echo Japanese ;;\n    ko-kr) echo Korean ;;\n    lt-lt) echo Lithuanian ;;\n    lv-lv) echo Latvian ;;\n    nb-no) echo Norwegian ;;\n    nl-nl) echo Dutch ;;\n    pl-pl) echo Polish ;;\n    pt-pt) echo Portuguese ;;\n    pt-br) echo Brazilian ;;\n    ro-ro) echo Romanian ;;\n    ru-ru) echo Russian ;;\n    sk-sk) echo Slovak ;;\n    sl-si) echo Slovenian ;;\n    sr-latn | sr-latn-rs) echo Serbian_Latin ;;\n    sv-se) echo Swedish ;;\n    th-th) echo Thai ;;\n    tr-tr) echo Turkish ;;\n    uk-ua) echo Ukrainian ;;\n    zh-cn) echo ChnSimp ;;\n    zh-hk | zh-tw) echo ChnTrad ;;\n    esac\n}\n\nparse_windows_image_name() {\n    set -- $image_name\n\n    if ! [ \"$1\" = windows ]; then\n        return 1\n    fi\n    shift\n\n    if [ \"$1\" = server ]; then\n        server=server\n        shift\n    fi\n\n    version=$1\n    shift\n\n    if [ \"$1\" = r2 ]; then\n        version+=\" r2\"\n        shift\n    fi\n\n    edition=\n    while [ $# -gt 0 ]; do\n        case \"$1\" in\n        # windows 10 enterprise n ltsc 2021\n        k | n | kn) ;;\n        *)\n            if [ -n \"$edition\" ]; then\n                edition+=\" \"\n            fi\n            edition+=\"$1\"\n            ;;\n        esac\n        shift\n    done\n}\n\nis_have_arm_version() {\n    case \"$version\" in\n    10)\n        case \"$edition\" in\n        home | 'home single language' | pro | education | enterprise | 'pro education' | 'pro for workstations') return ;;\n        'iot enterprise') return ;;\n        # arm ltsc 只有 2021 有 iso\n        'enterprise ltsc 2021' | 'iot enterprise ltsc 2021') return ;;\n        esac\n        ;;\n    11) return ;;\n    esac\n    return 1\n}\n\nfind_windows_iso() {\n    parse_windows_image_name || error_and_exit \"--image-name wrong: $image_name\"\n    if ! { [ \"$version\" = 8 ] || [ \"$version\" = 8.1 ]; } && [ -z \"$edition\" ]; then\n        error_and_exit \"Edition is not set.\"\n    fi\n\n    if [ -z \"$lang\" ]; then\n        lang=en-us\n    fi\n    langs=\"$lang $(en_us) $(us) $(en_en) $(en)\"\n    langs=$(echo \"$langs\" | xargs -n 1 | awk '!seen[$0]++')\n    full_lang=$(english)\n\n    case \"$basearch\" in\n    x86_64)\n        arch_win=x64\n        arch_win_vlsc=64bit\n        ;;\n    aarch64)\n        arch_win=arm64\n        arch_win_vlsc=arm64\n        ;;\n    esac\n\n    get_windows_iso_link\n}\n\nget_windows_iso_link() {\n    get_label_msdn() {\n        if [ -n \"$server\" ]; then\n            case \"$version\" in\n            2019 | 2022 | 2025)\n                case \"$edition\" in\n                serverstandard | serverstandardcore) echo _ ;;\n                serverdatacenter | serverdatacentercore) echo _ ;;\n                esac\n                ;;\n            esac\n        else\n            case \"$version\" in\n            10)\n                case \"$edition\" in\n                home | 'home single language') echo consumer ;;\n                pro | enterprise) echo business ;;\n                education | 'pro education' | 'pro for workstations')\n                    case \"$arch_win\" in\n                    arm64) echo consumer ;;\n                    x64) echo business ;; # iso 更小\n                    esac\n                    ;;\n                # iot\n                'iot enterprise') echo 'iot enterprise' ;;\n                # iot ltsc\n                'iot enterprise ltsc 2021') echo \"$edition\" ;;\n                # ltsc\n                'enterprise ltsc 2021')\n                    # arm64 的 enterprise ltsc 2021 要下载 iot enterprise ltsc 2021 iso\n                    case \"$arch_win\" in\n                    arm64) echo 'iot enterprise ltsc 2021' ;;\n                    x86 | x64) echo 'enterprise ltsc 2021' ;;\n                    esac\n                    ;;\n                esac\n                ;;\n            11)\n                # arm business iso 都没有 education, pro education, pro for workstations\n                # 即使它的名字包含 EDU\n                # SW_DVD9_Win_Pro_10_22H2.31_Arm64_English_Pro_Ent_EDU_N_MLF_X24-05074.ISO\n                # en-us_windows_11_business_editions_version_25h2_arm64_dvd_8afc9b39.iso\n                case \"$edition\" in\n                home | 'home single language') echo consumer ;;\n                pro | enterprise) echo business ;;\n                education | 'pro education' | 'pro for workstations')\n                    case \"$arch_win\" in\n                    arm64) echo consumer ;;\n                    x64) echo business ;; # iso 更小\n                    esac\n                    ;;\n                # iot\n                'iot enterprise' | 'iot enterprise subscription') echo 'iot enterprise' ;;\n                # iot ltsc\n                'iot enterprise ltsc 2024' | 'iot enterprise subscription ltsc 2024') echo 'iot enterprise ltsc 2024' ;;\n                # ltsc\n                'enterprise ltsc 2024')\n                    # arm64 的 enterprise ltsc 2024 要下载 iot enterprise ltsc 2024 iso\n                    case \"$arch_win\" in\n                    arm64) echo 'iot enterprise ltsc 2024' ;;\n                    x64) echo 'enterprise ltsc 2024' ;;\n                    esac\n                    ;;\n                esac\n                ;;\n            esac\n        fi\n    }\n\n    get_label_vlsc() {\n        case \"$version\" in\n        10 | 11)\n            case \"$edition\" in\n            pro | education | enterprise | 'pro education' | 'pro for workstations') echo pro ;;\n            esac\n            ;;\n        2025)\n            echo SrvSTDCORE\n            ;;\n        esac\n    }\n\n    # msdl 没有每月发布的 iso\n    # msdl 只有 consumer 版本，因此里面的 pro 版本不是 vl 版\n    # 8.1 没有每月发布的 iso，因此优先从 msdl 下载\n    # win10 22h2 arm 有每月发布的 iso，因此不从 msdl 下载\n    # win10/11 ltsc 没有每月发布的 iso，但是 msdl 没有 ltsc 版本\n    get_label_msdl() {\n        :\n    }\n\n    get_page() {\n        if [ \"$arch_win\" = arm64 ]; then\n            echo arm\n        elif is_ltsc; then\n            echo ltsc\n        elif [ \"$server\" = 'server' ]; then\n            echo server\n        else\n            case \"$version\" in\n            10 | 11)\n                echo \"$version\"\n                ;;\n            esac\n        fi\n    }\n\n    is_ltsc() {\n        grep -Ewq 'ltsb|ltsc' <<<\"$edition\"\n    }\n\n    # 部分 bash 不支持 $() 里面嵌套case，所以定义成函数\n    label_msdn=$(get_label_msdn)\n    label_msdl=$(get_label_msdl)\n    label_vlsc=$(get_label_vlsc)\n    page=$(get_page)\n\n    if [ \"$page\" = server ]; then\n        page_url=https://massgrave.dev/windows-server-links\n    else\n        page_url=https://massgrave.dev/windows_${page}_links\n    fi\n\n    info \"Find windows iso\"\n    echo \"Version:    $version\"\n    echo \"Edition:    $edition\"\n    echo \"Label msdn: $label_msdn\"\n    echo \"Label msdl: $label_msdl\"\n    echo \"Label vlsc: $label_vlsc\"\n    echo \"List:       $page_url\"\n    echo\n\n    # 先判断是否能自动查找该版本\n    # 再判断是否支持 arm\n    # 这样可以在输入错误 Edition 时例如 windows 11 enterprise ltsc 2021\n    # 显示名称错误，而不是显示该版本不支持 arm\n\n    if [ -z \"$page\" ] || { [ -z \"$label_msdn\" ] && [ -z \"$label_msdl\" ] && [ -z \"$label_vlsc\" ]; }; then\n        error_and_exit \"Not support find this iso. Check if --image-name is wrong. Or set --iso manually.\"\n    fi\n\n    if [ \"$basearch\" = aarch64 ] && ! is_have_arm_version; then\n        error_and_exit \"No ARM iso for this Windows Version or Edition.\"\n    fi\n\n    if [ -n \"$label_msdl\" ]; then\n        iso=$(curl -L \"$page_url\" | grep -ioP 'https://[^ ]+?#[0-9]+' | head -1 | grep .)\n    else\n        http_to_host=$(get_scheme_and_host_by_url \"$page_url\")\n        http_to_current_dir=$(dirname \"$page_url\")\n        curl -L \"$page_url\" |\n            tr -d '\\n' | sed -e 's,<a ,\\n<a ,g' -e 's,</a>,</a>\\n,g' | # 使每个 <a></a> 占一行\n            grep -Ei '\\.(iso|img)</a>$' |                              # 找出是 iso 或 img 的行\n            # 提取文件名和链接\n            # 如果链接是 / 开头，则补全域名\n            # 如果链接非 https:// 开头，则补全域名和目录\n            sed -E -e 's,<a href=\"?([^\" ]+)\"?.+>(.+)</a>,\\2 \\1,' \\\n                -e \"s, (/), $http_to_host\\1,\" |\n            awk '{if ($2 !~ /^https?:\\/\\//) $2 = \"'$http_to_current_dir/'\" $2; print}' >$tmp/win.list\n\n        # 如果不是 ltsc ，应该先去除 ltsc 链接，否则最终链接有 ltsc 的\n        # 例如查找 windows 10 iot enterprise，会得到\n        # en-us_windows_10_iot_enterprise_ltsc_2021_arm64_dvd_e8d4fc46.iso\n        # en-us_windows_10_iot_enterprise_version_22h2_arm64_dvd_39566b6b.iso\n        # sed -Ei 和 sed -iE 是不同的\n        if is_ltsc; then\n            sed -Ei '/ltsc|ltsb/!d' $tmp/win.list\n        else\n            sed -Ei '/ltsc|ltsb/d' $tmp/win.list\n        fi\n\n        get_windows_iso_link_inner\n    fi\n}\n\nget_shortest_line() {\n    awk '(NR == 1 || length($0) < length(shortest)) { shortest = $0 } END { print shortest }'\n}\n\nget_shortest_line_by_field() {\n    local field=$1\n    awk \"(NR == 1 || length(\\$$field) < length(field)) { line = \\$0; field = \\$$field } END { print line }\"\n}\n\nget_windows_iso_link_inner() {\n    regexs=()\n\n    # msdn\n    if [ -n \"$label_msdn\" ]; then\n        if [ \"$label_msdn\" = _ ]; then\n            label_msdn=\n        fi\n        for lang in $langs; do\n            regex=\n            for i in ${lang} windows ${server} ${version} ${label_msdn}; do\n                if [ -n \"$i\" ]; then\n                    regex+=\"${i}_\"\n                fi\n            done\n            regex+=\".*${arch_win}.*.(iso|img)\"\n            regexs+=(\"$regex\")\n        done\n    fi\n\n    # vlsc\n    # SW_DVD5_Win_10_IOT_Enterprise_2015_LTSB_64Bit_EMB_English_OEM_X20-20063.IMG\n    # SW_DVD9_Win_Pro_10_22H2.15_Arm64_English_Pro_Ent_EDU_N_MLF_X23-67223.ISO\n    # SWDVD9_WinSrvSTDCORE2025_24H2.16_64Bit_English_DC_STD_MLF_RTMUpdJan26_X24-26760.iso\n\n    # 先判断 full_lang 是否为空\n    # 因为假如用户输入的 lang 不正确，full_lang 就为空，正则表达式就无法只匹配当前语言\n    if [ -n \"$label_vlsc\" ] && [ -n \"$full_lang\" ]; then\n        regex=\"sw_?dvd[59]_win_?${label_vlsc}_?${version}.*${arch_win_vlsc}_${full_lang}.*.(iso|img)\"\n        regexs+=(\"$regex\")\n    fi\n\n    # 查找\n    for regex in \"${regexs[@]}\"; do\n        regex=${regex// /_}\n\n        echo \"looking for: $regex\" >&2\n        if line=$(grep -Ei \"^$regex \" \"$tmp/win.list\" | get_shortest_line_by_field 1 | grep .) &&\n            iso=$(awk '{print $2}' <<<\"$line\" | grep .); then\n            echo \"Selected: $line\" >&2\n            return\n        fi\n    done\n\n    error_and_exit \"Could not find iso for this windows edition or language.\"\n}\n\nsetos() {\n    local step=$1\n    local distro=$2\n    local releasever=$3\n    info set $step $distro $releasever\n\n    setos_netboot.xyz() {\n        if is_efi; then\n            if [ \"$basearch\" = aarch64 ]; then\n                eval ${step}_efi=https://boot.netboot.xyz/ipxe/netboot.xyz-arm64.efi\n            else\n                eval ${step}_efi=https://boot.netboot.xyz/ipxe/netboot.xyz.efi\n            fi\n        else\n            eval ${step}_vmlinuz=https://boot.netboot.xyz/ipxe/netboot.xyz.lkrn\n        fi\n    }\n\n    setos_alpine() {\n        is_virt && flavour=virt || flavour=lts\n\n        # 不要用https 因为甲骨文云arm initramfs阶段不会从硬件同步时钟，导致访问https出错\n        if is_in_china; then\n            mirror=http://mirror.nju.edu.cn/alpine/v$releasever\n        else\n            mirror=http://dl-cdn.alpinelinux.org/alpine/v$releasever\n        fi\n        eval ${step}_vmlinuz=$mirror/releases/$basearch/netboot/vmlinuz-$flavour\n        eval ${step}_initrd=$mirror/releases/$basearch/netboot/initramfs-$flavour\n        eval ${step}_modloop=$mirror/releases/$basearch/netboot/modloop-$flavour\n        eval ${step}_repo=$mirror/main\n    }\n\n    setos_debian() {\n        is_debian_elts() {\n            [ \"$releasever\" -le 10 ]\n        }\n\n        if [ \"$releasever\" -le 9 ] && [ \"$basearch\" = aarch64 ]; then\n            error_and_exit \"Debian $releasever ELTS does not support aarch64.\"\n        fi\n\n        # 用此标记要是否 elts, 用于安装后修改 elts/etls-cn 源\n        # shellcheck disable=SC2034\n        is_debian_elts && elts=1 || elts=0\n\n        case \"$releasever\" in\n        9) codename=stretch ;;\n        10) codename=buster ;;\n        11) codename=bullseye ;;\n        12) codename=bookworm ;;\n        13) codename=trixie ;;\n        14) codename=forky ;;\n        15) codename=duke ;;\n        esac\n\n        if ! is_use_cloud_image && is_debian_elts && is_in_china; then\n            warn \"\nDue to the lack of Debian Freexian ELTS instaler mirrors in China, the installation time may be longer.\nContinue?\n\n由于没有 Debian Freexian ELTS 国内安装源，安装时间可能会比较长。\n继续安装?\n\"\n            read -r -p '[y/N]: '\n            if ! [[ \"$REPLY\" = [Yy] ]]; then\n                exit\n            fi\n        fi\n\n        # udeb_mirror 安装时的源\n        # deb_mirror 安装后要修改成的源\n        if is_debian_elts; then\n            if is_in_china; then\n                # https://github.com/tuna/issues/issues/1999\n                # nju 也没同步\n                udeb_mirror=deb.freexian.com/extended-lts\n                deb_mirror=mirror.nju.edu.cn/debian-elts\n                initrd_mirror=mirror.nju.edu.cn/debian-archive/debian\n            else\n                # 按道理不应该用官方源，但找不到其他源\n                udeb_mirror=deb.freexian.com/extended-lts\n                deb_mirror=deb.freexian.com/extended-lts\n                initrd_mirror=archive.debian.org/debian\n            fi\n        else\n            if is_in_china; then\n                # ftp.cn.debian.org 不在国内还严重丢包\n                # https://www.itdog.cn/ping/ftp.cn.debian.org\n                mirror=mirror.nju.edu.cn/debian\n            else\n                mirror=deb.debian.org/debian # fastly\n            fi\n            udeb_mirror=$mirror\n            deb_mirror=$mirror\n            initrd_mirror=$mirror\n        fi\n\n        # 云镜像和 firmware 下载源\n        if is_in_china; then\n            cdimage_mirror=https://mirror.nju.edu.cn/debian-cdimage\n        else\n            cdimage_mirror=https://cdimage.debian.org/images # 在瑞典，不是 cdn\n            # cloud.debian.org 同样在瑞典，不是 cdn\n        fi\n\n        is_virt && flavour=-cloud || flavour=\n        # debian 10 云内核 vultr efi vnc 没有显示\n        [ \"$releasever\" -le 10 ] && flavour=\n        # 甲骨文 arm64 cloud 内核 vnc 没有显示\n        [ \"$basearch_alt\" = arm64 ] && flavour=\n\n        if is_use_cloud_image; then\n            # cloud image\n            # https://salsa.debian.org/cloud-team/debian-cloud-images/-/tree/master/config_space/bookworm/files/etc/default/grub.d\n            # cloud 包括各种奇怪的优化，例如不显示 grub 菜单\n            # 因此使用 nocloud\n            if false; then\n                is_virt && ci_type=genericcloud || ci_type=generic\n            else\n                ci_type=nocloud\n            fi\n            eval ${step}_img=$cdimage_mirror/cloud/$codename/latest/debian-$releasever-$ci_type-$basearch_alt.qcow2\n        else\n            # 传统安装\n            initrd_dir=dists/$codename/main/installer-$basearch_alt/current/images/netboot/debian-installer/$basearch_alt\n\n            eval ${step}_udeb_mirror=$udeb_mirror\n            eval ${step}_vmlinuz=https://$initrd_mirror/$initrd_dir/linux\n            eval ${step}_initrd=https://$initrd_mirror/$initrd_dir/initrd.gz\n            eval ${step}_ks=$confhome/debian.cfg\n            eval ${step}_firmware=$cdimage_mirror/unofficial/non-free/firmware/$codename/current/firmware.cpio.gz\n            eval ${step}_codename=$codename\n        fi\n\n        # 官方安装和云镜像都会用到的\n        eval ${step}_deb_mirror=$deb_mirror\n        eval ${step}_kernel=linux-image$flavour-$basearch_alt\n    }\n\n    setos_kali() {\n        if is_use_cloud_image; then\n            :\n        else\n            # 传统安装\n            if is_in_china; then\n                hostname=mirror.nju.edu.cn\n            else\n                # http.kali.org 没有 ipv6 地址\n                # http.kali.org (geoip 重定向) 到 kali.download (cf)\n                hostname=kali.download\n            fi\n            codename=kali-rolling\n            mirror=http://$hostname/kali/dists/$codename/main/installer-$basearch_alt/current/images/netboot/debian-installer/$basearch_alt\n\n            is_virt && flavour=-cloud || flavour=\n\n            eval ${step}_vmlinuz=$mirror/linux\n            eval ${step}_initrd=$mirror/initrd.gz\n            eval ${step}_ks=$confhome/debian.cfg\n            eval ${step}_deb_mirror=$hostname/kali\n            eval ${step}_udeb_mirror=$hostname/kali\n            eval ${step}_codename=$codename\n            eval ${step}_kernel=linux-image$flavour-$basearch_alt\n            # 缺少 firmware 下载\n        fi\n    }\n\n    setos_ubuntu() {\n        case \"$releasever\" in\n        16.04) codename=xenial ;;\n        18.04) codename=bionic ;;\n        20.04) codename=focal ;;\n        22.04) codename=jammy ;;\n        24.04) codename=noble ;;\n        25.10) codename=questing ;; # non-lts\n        esac\n\n        if is_use_cloud_image; then\n            # cloud image\n            if is_in_china; then\n                # 有的源没有 releases 镜像\n                # https://mirrors.tuna.tsinghua.edu.cn/ubuntu-cloud-images/releases/\n                #   https://unicom.mirrors.ustc.edu.cn/ubuntu-cloud-images/releases/\n                #            https://mirror.nju.edu.cn/ubuntu-cloud-images/releases/\n\n                # mirrors.cloud.tencent.com\n                ci_mirror=https://mirror.nju.edu.cn/ubuntu-cloud-images\n            else\n                ci_mirror=https://cloud-images.ubuntu.com\n            fi\n\n            # 以下版本有 minimal 镜像\n            # amd64 所有\n            # arm64 24.04 和以上\n            is_have_minimal_image() {\n                [ \"$basearch_alt\" = amd64 ] || [ \"${releasever%.*}\" -ge 24 ]\n            }\n\n            get_suffix() {\n                if [ \"$releasever\" = 16.04 ]; then\n                    if is_efi; then\n                        echo -uefi1\n                    else\n                        echo -disk1\n                    fi\n                fi\n            }\n\n            if [ \"$minimal\" = 1 ]; then\n                if ! is_have_minimal_image; then\n                    error_and_exit \"Minimal cloud image is not available for $releasever $basearch_alt.\"\n                fi\n                eval ${step}_img=\"$ci_mirror/minimal/releases/$codename/release/ubuntu-$releasever-minimal-cloudimg-$basearch_alt$(get_suffix).img\"\n            else\n                # 用 codename 而不是 releasever，可减少一次跳转\n                eval ${step}_img=\"$ci_mirror/releases/$codename/release/ubuntu-$releasever-server-cloudimg-$basearch_alt$(get_suffix).img\"\n            fi\n        else\n            # 传统安装\n            if is_in_china; then\n                case \"$basearch\" in\n                \"x86_64\") mirror=https://mirror.nju.edu.cn/ubuntu-releases/$releasever ;;\n                \"aarch64\") mirror=https://mirror.nju.edu.cn/ubuntu-cdimage/releases/$releasever/release ;;\n                esac\n            else\n                case \"$basearch\" in\n                \"x86_64\") mirror=https://releases.ubuntu.com/$releasever ;;\n                \"aarch64\") mirror=https://cdimage.ubuntu.com/releases/$releasever/release ;;\n                esac\n            fi\n\n            # iso\n            filename=$(curl -L $mirror/ | grep -oP \"ubuntu-$releasever.*?-live-server-$basearch_alt.iso\" |\n                sort -uV | tail -1 | grep .)\n            iso=$mirror/$filename\n            # 在 ubuntu 20.04 上，file 命令检测 ubuntu 22.04 iso 结果是 DOS/MBR boot sector\n            test_url \"$iso\" iso\n            eval ${step}_iso=$iso\n\n            # ks\n            eval ${step}_ks=$confhome/ubuntu.yaml\n            eval ${step}_minimal=$minimal\n        fi\n    }\n\n    setos_arch() {\n        if [ \"$basearch\" = \"x86_64\" ]; then\n            if is_in_china; then\n                mirror=https://mirror.nju.edu.cn/archlinux\n            else\n                mirror=https://geo.mirror.pkgbuild.com # geoip\n            fi\n        else\n            if is_in_china; then\n                mirror=https://mirror.nju.edu.cn/archlinuxarm\n            else\n                # https 证书有问题\n                mirror=http://mirror.archlinuxarm.org # geoip\n            fi\n        fi\n\n        if is_use_cloud_image; then\n            # cloud image\n            eval ${step}_img=$mirror/images/latest/Arch-Linux-x86_64-cloudimg.qcow2\n        else\n            # 传统安装\n            case \"$basearch\" in\n            x86_64) dir=\"core/os/$basearch\" ;;\n            aarch64) dir=\"$basearch/core\" ;;\n            esac\n            test_url $mirror/$dir/core.db gzip\n            eval ${step}_mirror=$mirror\n        fi\n    }\n\n    setos_nixos() {\n        if is_in_china; then\n            mirror=https://mirror.nju.edu.cn/nix-channels\n        else\n            mirror=https://nixos.org/channels\n        fi\n\n        if is_use_cloud_image; then\n            :\n        else\n            # 传统安装\n            # 该服务器文件缓存 miss 时会响应 206 + Location 头\n            # 但 curl 这种情况不会重定向，所以添加 text 类型让它不要报错\n            test_url $mirror/nixos-$releasever/store-paths.xz 'xz text'\n            eval ${step}_mirror=$mirror\n        fi\n    }\n\n    setos_gentoo() {\n        if is_in_china; then\n            mirror=https://mirror.nju.edu.cn/gentoo\n        else\n            mirror=https://distfiles.gentoo.org # cdn77\n        fi\n\n        dir=releases/$basearch_alt/autobuilds\n\n        if is_use_cloud_image; then\n            # 使用 systemd 且没有 cloud-init\n            prefix=di-$basearch_alt-console\n            filename=$(curl -L $mirror/$dir/latest-$prefix.txt | grep '.qcow2' | awk '{print $1}' | grep .)\n            file=$mirror/$dir/$filename\n            test_url \"$file\" 'qemu'\n            eval ${step}_img=$file\n        else\n            prefix=stage3-$basearch_alt-systemd\n            filename=$(curl -L $mirror/$dir/latest-$prefix.txt | grep '.tar.xz' | awk '{print $1}' | grep .)\n            file=$mirror/$dir/$filename\n            test_url \"$file\" 'tar.xz'\n            eval ${step}_img=$file\n        fi\n    }\n\n    setos_opensuse() {\n        # https://download.opensuse.org/\n        # curl 会跳转到最近的镜像源，但可能会被镜像源 block\n        # aria2 会跳转使用 metalink\n\n        # https://downloadcontent.opensuse.org    # 德国\n        # https://downloadcontentcdn.opensuse.org # fastly cdn\n\n        # 很多国内源缺少 aarch64 tumbleweed appliances\n        #                 https://download.opensuse.org/ports/aarch64/tumbleweed/appliances/\n        #          https://mirrors.ustc.edu.cn/opensuse/ports/aarch64/tumbleweed/appliances/\n        # https://mirrors.tuna.tsinghua.edu.cn/opensuse/ports/aarch64/tumbleweed/appliances/\n\n        if is_in_china; then\n            mirror=https://mirror.nju.edu.cn/opensuse\n        else\n            mirror=https://downloadcontentcdn.opensuse.org\n        fi\n\n        if [ \"$releasever\" = tumbleweed ]; then\n            # tumbleweed\n            if [ \"$basearch\" = aarch64 ]; then\n                dir=ports/aarch64/tumbleweed/appliances\n            else\n                dir=tumbleweed/appliances\n            fi\n            file=openSUSE-Tumbleweed-Minimal-VM.$basearch-Cloud.qcow2\n        else\n            # leap\n            dir=distribution/leap/$releasever/appliances\n            case \"$releasever\" in\n            15.6) file=openSUSE-Leap-$releasever-Minimal-VM.$basearch-Cloud.qcow2 ;;\n            16.0) file=Leap-$releasever-Minimal-VM.$basearch-Cloud.qcow2 ;;\n            # 16.0) file=Leap-$releasever-Minimal-VM.$basearch-kvm$(if [ \"$basearch\" = x86_64 ]; then echo '-and-xen'; fi).qcow2 ;;\n            esac\n\n            # https://src.opensuse.org/openSUSE/Leap-Images/src/branch/leap-16.0/kiwi-templates-Minimal/Minimal.kiwi\n            # https://build.opensuse.org/projects/Virtualization:Appliances:Images:openSUSE-Leap-15.6/packages/kiwi-templates-Minimal/files/Minimal.kiwi\n            # https://build.opensuse.org/projects/Virtualization:Appliances:Images:openSUSE-Tumbleweed/packages/kiwi-templates-Minimal/files/Minimal.kiwi\n            # 有专门的kvm镜像，openSUSE-Leap-15.5-Minimal-VM.x86_64-kvm-and-xen.qcow2，里面没有cloud-init\n            # file=openSUSE-Leap-15.5-Minimal-VM.x86_64-kvm-and-xen.qcow2\n        fi\n        eval ${step}_img=$mirror/$dir/$file\n    }\n\n    setos_windows() {\n        auto_find_iso=false\n        if [ -z \"$iso\" ]; then\n            auto_find_iso=true\n            # 查找时将 windows longhorn serverdatacenter 改成 windows server 2008 serverdatacenter\n            image_name=${image_name/windows longhorn server/windows server 2008 server}\n            echo \"iso url is not set. Attempting to find it automatically.\"\n            find_windows_iso\n        fi\n\n        # 将上面的 windows server 2008 serverdatacenter 改回 windows longhorn serverdatacenter\n        # 也能纠正用户输入了 windows server 2008 serverdatacenter\n        # 注意 windows server 2008 r2 serverdatacenter 不用改\n        image_name=${image_name/windows server 2008 server/windows longhorn server}\n\n        if [[ \"$iso\" = magnet:* ]]; then\n            : # 不测试磁力链接\n        else\n            iso_is_tested=false\n            if $auto_find_iso; then\n                if test_url_grace \"$iso\" iso 2>/dev/null; then\n                    iso_is_tested=true\n                else\n                    # 需要用户输入 massgrave.dev 直链\n                    info \"Set Direct link\"\n                    # MobaXterm 不支持\n                    # printf '\\e]8;;http://example.com\\e\\\\This is a link\\e]8;;\\e\\\\\\n'\n\n                    # MobaXterm 不显示为超链接\n                    # info false \"请在浏览器中打开 $iso 获取直链并粘贴到这里。\"\n                    # info false \"Please open $iso in browser to get the direct link and paste it here.\"\n\n                    echo \"请在浏览器中打开 $iso 获取直链并粘贴到这里。\"\n                    echo \"Please open $iso in browser to get the direct link and paste it here.\"\n                    IFS= read -r -p \"Direct Link: \" iso\n                    if [ -z \"$iso\" ]; then\n                        error_and_exit \"ISO Link is empty.\"\n                    fi\n                fi\n            fi\n\n            if ! $iso_is_tested; then\n                test_url \"$iso\" iso\n            fi\n\n            # 判断 iso 架构是否兼容\n            # https://gitlab.com/libosinfo/osinfo-db/-/tree/main/data/os/microsoft.com?ref_type=heads\n            # uupdump linux 下合成的标签是 ARM64，windows下合成的标签是 A64\n            if file -b \"$tmp/img-test\" | grep -Eq '_(A64|ARM64)'; then\n                iso_arch=arm64\n            else\n                iso_arch=x86_or_x64\n            fi\n\n            if ! {\n                { [ \"$basearch\" = x86_64 ] && [ \"$iso_arch\" = x86_or_x64 ]; } ||\n                    { [ \"$basearch\" = aarch64 ] && [ \"$iso_arch\" = arm64 ]; }\n            }; then\n                warn \"\nThe current machine is $basearch, but it seems the ISO is for $iso_arch. Continue?\n当前机器是 $basearch，但 ISO 似乎是 $iso_arch。继续安装?\"\n                read -r -p '[y/N]: '\n                if ! [[ \"$REPLY\" = [Yy] ]]; then\n                    exit\n                fi\n            fi\n        fi\n\n        [ -n \"$boot_wim\" ] && test_url \"$boot_wim\" 'wim'\n\n        eval \"${step}_iso='$iso'\"\n        eval \"${step}_boot_wim='$boot_wim'\"\n        eval \"${step}_image_name='$image_name'\"\n    }\n\n    # shellcheck disable=SC2154\n    setos_dd() {\n        # raw 包含 vhd\n        test_url $img 'raw raw.gzip raw.xz raw.zstd raw.tar.gzip raw.tar.xz raw.tar.zstd' img_type\n\n        if is_efi; then\n            install_pkg hexdump\n\n            # openwrt 镜像 efi part type 不是 esp\n            # 因此改成检测 fat?\n            # https://downloads.openwrt.org/releases/23.05.3/targets/x86/64/openwrt-23.05.3-x86-64-generic-ext4-combined-efi.img.gz\n\n            # od 在 coreutils 里面，好像要配合 tr 才能删除空格\n            # hexdump 在 util-linux / bsdmainutils 里面\n            # xxd 要单独安装，el 在 vim-common 里面\n            # xxd -l $((34 * 4096)) -ps -c 128\n\n            # 仅打印前34个扇区 * 4096字节（按最大的算）\n            # 每行128字节\n            hexdump -n $((34 * 4096)) -e '128/1 \"%02x\" \"\\n\"' -v \"$tmp/img-test\" >$tmp/img-test-hex\n            if grep -q '^28732ac11ff8d211ba4b00a0c93ec93b' $tmp/img-test-hex; then\n                echo 'DD: Image is EFI.'\n            else\n                echo 'DD: Image is not EFI.'\n                warn '\nThe current machine uses EFI boot, but the DD image seems not an EFI image.\nContinue with DD?\n当前机器使用 EFI 引导，但 DD 镜像可能不是 EFI 镜像。\n继续 DD?'\n                read -r -p '[y/N]: '\n                if [[ \"$REPLY\" = [Yy] ]]; then\n                    eval ${step}_confirmed_no_efi=1\n                else\n                    exit\n                fi\n            fi\n        fi\n        eval \"${step}_img='$img'\"\n        eval \"${step}_img_type='$img_type'\"\n        eval \"${step}_img_type_warp='$img_type_warp'\"\n    }\n\n    setos_fnos() {\n        # 系统盘大小\n        min=8\n        default=8\n        echo \"请输入系统分区大小，最小 $min GB，但可能无法更新系统。\"\n        echo \"Please input System Partition Size. Minimal is $min GB but may not be able to do system updates.\"\n        while true; do\n            IFS= read -r -p \"Size in GB [$default]: \" input\n            input=${input:-$default}\n            if ! { is_digit \"$input\" && [ \"$input\" -ge \"$min\" ]; }; then\n                error \"Invalid Size. Please Try again.\"\n            else\n                eval \"${step}_fnos_part_size=${input}G\"\n                break\n            fi\n        done\n\n        if [ -z \"$iso\" ]; then\n            # 对于同一行有多个成功匹配，grep -m1 无效\n            iso=$(curl -L \"https://fnnas.com/download$([ \"$basearch\" = aarch64 ] && echo -arm)\" |\n                grep -o 'https://[^\"]*\\.iso' | head -1 | grep .)\n\n            # curl 7.82.0+\n            # curl -L --json '{\"url\":\"'$iso'\"}' https://www.fnnas.com/api/download-sign\n\n            iso=$(curl -L \\\n                -d '{\"url\":\"'$iso'\"}' \\\n                -H 'Content-Type: application/json' \\\n                https://www.fnnas.com/api/download-sign |\n                grep -o 'https://[^\"]*')\n        fi\n\n        test_url \"$iso\" iso\n        eval \"${step}_iso='$iso'\"\n    }\n\n    setos_aosc() {\n        if is_in_china; then\n            mirror=https://mirror.nju.edu.cn/anthon/aosc-os\n        else\n            # 服务器在香港\n            mirror=https://releases.aosc.io\n        fi\n\n        dir=os-$basearch_alt/base\n        file=$(curl -L $mirror/$dir/ | grep -oP 'aosc-os_base_.*?\\.tar.xz' |\n            sort -uV | tail -1 | grep .)\n        img=$mirror/$dir/$file\n        test_url $img 'tar.xz'\n        eval ${step}_img=$img\n    }\n\n    setos_centos_almalinux_rocky_fedora() {\n        # el 10 需要 x86-64-v3，除了 almalinux\n        if [ \"$basearch\" = x86_64 ] &&\n            { [ \"$distro\" = centos ] || [ \"$distro\" = rocky ]; } &&\n            [ \"$releasever\" -ge 10 ]; then\n            assert_cpu_supports_x86_64_v3\n        fi\n\n        elarch=$basearch\n        if [ \"$basearch\" = x86_64 ] &&\n            [ \"$distro\" = almalinux ] && [ \"$releasever\" -ge 10 ] &&\n            ! is_cpu_supports_x86_64_v3; then\n            elarch=x86_64_v2\n        fi\n\n        if is_use_cloud_image; then\n            # ci\n            if is_in_china; then\n                case $distro in\n                centos) ci_mirror=\"https://mirror.nju.edu.cn/centos-cloud/centos\" ;;\n                almalinux) ci_mirror=\"https://mirror.nju.edu.cn/almalinux/$releasever/cloud/$elarch/images\" ;;\n                rocky) ci_mirror=\"https://mirror.nju.edu.cn/rocky/$releasever/images/$elarch\" ;;\n                fedora) ci_mirror=\"https://mirror.nju.edu.cn/fedora/releases/$releasever/Cloud/$elarch/images\" ;;\n                esac\n            else\n                case $distro in\n                centos) ci_mirror=\"https://cloud.centos.org/centos\" ;;\n                almalinux) ci_mirror=\"https://repo.almalinux.org/almalinux/$releasever/cloud/$elarch/images\" ;;\n                rocky) ci_mirror=\"https://download.rockylinux.org/pub/rocky/$releasever/images/$elarch\" ;;\n                fedora) ci_mirror=\"https://d2lzkl7pfhq30w.cloudfront.net/pub/fedora/linux/releases/$releasever/Cloud/$elarch/images\" ;;\n                esac\n            fi\n            case $distro in\n            centos)\n                case $releasever in\n                7)\n                    # CentOS-7-aarch64-GenericCloud.qcow2c 是旧版本\n                    ver=-2211\n                    ci_image=$ci_mirror/$releasever/images/CentOS-$releasever-$elarch-GenericCloud$ver.qcow2c\n                    ;;\n                *)\n                    # 有 bios 和 efi 镜像\n                    # https://cloud.centos.org/centos/10-stream/x86_64/images/CentOS-Stream-GenericCloud-10-latest.x86_64.qcow2\n                    # https://cloud.centos.org/centos/10-stream/x86_64/images/CentOS-Stream-GenericCloud-x86_64-10-latest.x86_64.qcow2\n                    [ \"$elarch\" = x86_64 ] &&\n                        ci_image=$ci_mirror/$releasever-stream/$elarch/images/CentOS-Stream-GenericCloud-x86_64-$releasever-latest.$elarch.qcow2 ||\n                        ci_image=$ci_mirror/$releasever-stream/$elarch/images/CentOS-Stream-GenericCloud-$releasever-latest.$elarch.qcow2\n                    ;;\n                esac\n                ;;\n            almalinux) ci_image=$ci_mirror/AlmaLinux-$releasever-GenericCloud-latest.$elarch.qcow2 ;;\n            rocky) ci_image=$ci_mirror/Rocky-$releasever-GenericCloud-Base.latest.$elarch.qcow2 ;;\n            fedora)\n                # 不加 / 会跳转到 https://dl.fedoraproject.org，纯 ipv6 无法访问\n                # curl -L -6 https://d2lzkl7pfhq30w.cloudfront.net/pub/fedora/linux/releases/42/Cloud/x86_64/images\n                # curl -L -6 https://d2lzkl7pfhq30w.cloudfront.net/pub/fedora/linux/releases/42/Cloud/x86_64/images/\n                filename=$(curl -L $ci_mirror/ | grep -oP \"Fedora-Cloud-Base-Generic.*?.qcow2\" |\n                    sort -uV | tail -1 | grep .)\n                ci_image=$ci_mirror/$filename\n                ;;\n            esac\n\n            eval ${step}_img=${ci_image}\n        else\n            # 传统安装\n            case $distro in\n            centos) mirrorlist=\"https://mirrors.centos.org/mirrorlist?repo=centos-baseos-$releasever-stream&arch=$elarch\" ;;\n            almalinux) mirrorlist=\"https://mirrors.almalinux.org/mirrorlist/$releasever/baseos\" ;;\n            rocky) mirrorlist=\"https://mirrors.rockylinux.org/mirrorlist?arch=$elarch&repo=BaseOS-$releasever\" ;;\n            fedora) mirrorlist=\"https://mirrors.fedoraproject.org/mirrorlist?arch=$elarch&repo=fedora-$releasever\" ;;\n            esac\n\n            # rocky/centos9 需要删除第一行注释， almalinux 需要替换链接里面的 $basearch\n            for cur_mirror in $(curl -L $mirrorlist | sed \"/^#/d\" | sed \"s,\\$basearch,$elarch,\"); do\n                host=$(get_host_by_url $cur_mirror)\n                if is_host_has_ipv4_and_ipv6 $host &&\n                    test_url_grace ${cur_mirror}images/pxeboot/vmlinuz; then\n                    mirror=$cur_mirror\n                    break\n                fi\n            done\n\n            if [ -z \"$mirror\" ]; then\n                error_and_exit \"All mirror failed.\"\n            fi\n\n            eval \"${step}_mirrorlist='${mirrorlist}'\"\n\n            eval ${step}_ks=$confhome/redhat.cfg\n            eval ${step}_vmlinuz=${mirror}images/pxeboot/vmlinuz\n            eval ${step}_initrd=${mirror}images/pxeboot/initrd.img\n            eval ${step}_squashfs=${mirror}images/install.img\n            test_url ${mirror}images/install.img 'squashfs'\n        fi\n    }\n\n    setos_oracle() {\n        # el 10 需要 x86-64-v3\n        if [ \"$basearch\" = x86_64 ] && [ \"$releasever\" -ge 10 ]; then\n            assert_cpu_supports_x86_64_v3\n        fi\n\n        if is_use_cloud_image; then\n            # ci\n            install_pkg jq\n            mirror=https://yum.oracle.com\n\n            [ \"$basearch\" = aarch64 ] &&\n                template_prefix=ol${releasever}_${basearch}-cloud ||\n                template_prefix=ol${releasever}\n            curl -Lo $tmp/oracle.json $mirror/templates/OracleLinux/$template_prefix-template.json\n            dir=$(jq -r .base_url $tmp/oracle.json)\n            file=$(jq -r .kvm.image $tmp/oracle.json)\n            ci_image=$mirror$dir/$file\n\n            eval ${step}_img=${ci_image}\n        else\n            :\n        fi\n    }\n\n    setos_redhat() {\n        if is_use_cloud_image; then\n            # el 10 需要 x86-64-v3\n            if [ \"$basearch\" = x86_64 ] && [[ \"$img\" = *rhel-10* ]]; then\n                assert_cpu_supports_x86_64_v3\n            fi\n            eval \"${step}_img='$img'\"\n        else\n            :\n        fi\n    }\n\n    setos_opencloudos() {\n        # https://mirrors.opencloudos.tech 不支持 ipv6\n        # https://mirrors.cloud.tencent.com 没有 stream\n        if [ \"$releasever\" -ge 23 ]; then\n            mirror=https://mirrors.opencloudos.tech/opencloudos-stream/releases\n        else\n            mirror=https://mirrors.cloud.tencent.com/opencloudos\n        fi\n\n        if is_use_cloud_image; then\n            # ci\n            if [ \"$releasever\" -eq 9 ]; then\n                dir=$releasever/images/qcow2/$basearch\n            else\n                dir=$releasever/images/$basearch\n            fi\n\n            file=$(curl -L $mirror/$dir/ | grep -oP 'OpenCloudOS.*?\\.qcow2' |\n                sort -uV | tail -1 | grep .)\n            eval ${step}_img=$mirror/$dir/$file\n        else\n            :\n        fi\n    }\n\n    setos_anolis() {\n        mirror=https://mirrors.openanolis.cn/anolis\n        if is_use_cloud_image; then\n            # ci\n            dir=$releasever/isos/GA/$basearch\n            [ \"$releasever\" -ge 23 ] &&\n                filename='AnolisOS.*?\\.qcow2' ||\n                filename='AnolisOS.*?-ANCK\\.qcow2'\n            file=$(curl -L $mirror/$dir/ | grep -oP \"$filename\" |\n                sort -uV | tail -1 | grep .)\n            eval ${step}_img=$mirror/$dir/$file\n        else\n            :\n        fi\n    }\n\n    setos_openeuler() {\n        if is_in_china; then\n            mirror=https://repo.openeuler.openatom.cn\n        else\n            mirror=https://repo.openeuler.org\n        fi\n        if is_use_cloud_image; then\n            # ci\n            name=$(curl -L \"$mirror/\" | grep -oE \"openEuler-$releasever(-LTS)?(-SP[0-9])?\" |\n                sort -uV | tail -1 | grep .)\n            eval ${step}_img=$mirror/$name/virtual_machine_img/$basearch/$name-$basearch.qcow2.xz\n        else\n            :\n        fi\n    }\n\n    eval ${step}_distro=$distro\n    eval ${step}_releasever=$releasever\n\n    case \"$distro\" in\n    centos | almalinux | rocky | fedora) setos_centos_almalinux_rocky_fedora ;;\n    *) setos_$distro ;;\n    esac\n\n    # debian/kali <=256M 必须使用云内核，否则不够内存\n    if is_distro_like_debian && ! is_in_windows && [ \"$ram_size\" -le 256 ]; then\n        exit_if_cant_use_cloud_kernel\n    fi\n\n    # 集中测试云镜像格式\n    if is_use_cloud_image && [ \"$step\" = finalos ]; then\n        # shellcheck disable=SC2154\n        test_url $finalos_img 'qemu qemu.gzip qemu.xz qemu.zstd raw.xz' finalos_img_type\n    fi\n}\n\nis_distro_like_redhat() {\n    if [ -n \"$1\" ]; then\n        _distro=$1\n    else\n        _distro=$distro\n    fi\n    [ \"$_distro\" = redhat ] || [ \"$_distro\" = centos ] || [ \"$_distro\" = almalinux ] || [ \"$_distro\" = rocky ] || [ \"$_distro\" = fedora ] || [ \"$_distro\" = oracle ]\n}\n\nis_distro_like_debian() {\n    if [ -n \"$1\" ]; then\n        _distro=$1\n    else\n        _distro=$distro\n    fi\n    [ \"$_distro\" = debian ] || [ \"$_distro\" = kali ]\n}\n\nget_latest_distro_releasever() {\n    get_function_content verify_os_name |\n        grep -wo \"$1 [^'\\\"]*\" | awk -F'|' '{print $NF}'\n}\n\n# 检查是否为正确的系统名\nverify_os_name() {\n    if [ -z \"$*\" ]; then\n        usage_and_exit\n    fi\n\n    # 不要删除 centos 7\n    for os in \\\n        'centos      7|9|10' \\\n        'anolis      7|8|23' \\\n        'opencloudos 8|9|23' \\\n        'almalinux   8|9|10' \\\n        'rocky       8|9|10' \\\n        'oracle      8|9|10' \\\n        'fnos        1' \\\n        'fedora      42|43' \\\n        'nixos       25.11' \\\n        'debian      9|10|11|12|13' \\\n        'opensuse    15.6|16.0|tumbleweed' \\\n        'alpine      3.20|3.21|3.22|3.23' \\\n        'openeuler   20.03|22.03|24.03|25.09' \\\n        'ubuntu      16.04|18.04|20.04|22.04|24.04|25.10' \\\n        'redhat' \\\n        'kali' \\\n        'arch' \\\n        'gentoo' \\\n        'aosc' \\\n        'windows' \\\n        'dd' \\\n        'netboot.xyz' \\\n        'reset'; do\n        read -r ds vers <<<\"$os\"\n        vers_=${vers//\\./\\\\\\.}\n        finalos=$(echo \"$@\" | to_lower | sed -n -E \"s,^($ds)[ :-]?(|$vers_)$,\\1 \\2,p\")\n        if [ -n \"$finalos\" ]; then\n            read -r distro releasever <<<\"$finalos\"\n            # 默认版本号\n            if [ -z \"$releasever\" ] && [ -n \"$vers\" ]; then\n                releasever=$(awk -F '|' '{print $NF}' <<<\"|$vers\")\n            fi\n            return\n        fi\n    done\n\n    error \"Please specify a proper os\"\n    usage_and_exit\n}\n\nverify_os_args() {\n    case \"$distro\" in\n    dd) [ -n \"$img\" ] || error_and_exit \"dd need --img\" ;;\n    redhat) [ -n \"$img\" ] || error_and_exit \"redhat need --img\" ;;\n    windows) [ -n \"$image_name\" ] || error_and_exit \"Install Windows need --image-name.\" ;;\n    esac\n\n    case \"$distro\" in\n    netboot.xyz | windows) [ -z \"$ssh_keys\" ] || error_and_exit \"not support ssh key for $distro\" ;;\n    esac\n}\n\nget_cmd_path() {\n    # arch 云镜像不带 which\n    # command -v 包括脚本里面的方法\n    # ash 无效\n    type -f -p $1\n}\n\nis_have_cmd() {\n    get_cmd_path $1 >/dev/null 2>&1\n}\n\ninstall_pkg() {\n    is_in_windows && return\n\n    find_pkg_mgr() {\n        [ -n \"$pkg_mgr\" ] && return\n\n        # 查找方法1: 通过 ID / ID_LIKE\n        # 因为可能装了多种包管理器\n        if [ -f /etc/os-release ]; then\n            # shellcheck source=/dev/null\n            for id in $({ . /etc/os-release && echo $ID $ID_LIKE; }); do\n                # https://github.com/chef/os_release\n                case \"$id\" in\n                fedora | centos | rhel) is_have_cmd dnf && pkg_mgr=dnf || pkg_mgr=yum ;;\n                debian | ubuntu) pkg_mgr=apt-get ;;\n                opensuse | suse) pkg_mgr=zypper ;;\n                alpine) pkg_mgr=apk ;;\n                arch) pkg_mgr=pacman ;;\n                gentoo) pkg_mgr=emerge ;;\n                nixos) pkg_mgr=nix-env ;;\n                esac\n                [ -n \"$pkg_mgr\" ] && return\n            done\n        fi\n\n        # 查找方法 2\n        for mgr in dnf yum apt-get pacman zypper emerge apk nix-env; do\n            is_have_cmd $mgr && pkg_mgr=$mgr && return\n        done\n\n        return 1\n    }\n\n    cmd_to_pkg() {\n        unset USE\n        case $cmd in\n        ar)\n            case \"$pkg_mgr\" in\n            *) pkg=\"binutils\" ;;\n            esac\n            ;;\n        xz)\n            case \"$pkg_mgr\" in\n            apt-get) pkg=\"xz-utils\" ;;\n            *) pkg=\"xz\" ;;\n            esac\n            ;;\n        lsblk | findmnt)\n            case \"$pkg_mgr\" in\n            apk) pkg=\"$cmd\" ;;\n            *) pkg=\"util-linux\" ;;\n            esac\n            ;;\n        lsmem)\n            case \"$pkg_mgr\" in\n            apk) pkg=\"util-linux-misc\" ;;\n            *) pkg=\"util-linux\" ;;\n            esac\n            ;;\n        fdisk)\n            case \"$pkg_mgr\" in\n            apt-get) pkg=\"fdisk\" ;;\n            apk) pkg=\"util-linux-misc\" ;;\n            *) pkg=\"util-linux\" ;;\n            esac\n            ;;\n        hexdump)\n            case \"$pkg_mgr\" in\n            apt-get) pkg=\"bsdmainutils\" ;;\n            *) pkg=\"util-linux\" ;;\n            esac\n            ;;\n        unsquashfs)\n            case \"$pkg_mgr\" in\n            zypper) pkg=\"squashfs\" ;;\n            emerge) pkg=\"squashfs-tools\" && export USE=\"lzma\" ;;\n            *) pkg=\"squashfs-tools\" ;;\n            esac\n            ;;\n        nslookup | dig)\n            case \"$pkg_mgr\" in\n            apt-get) pkg=\"dnsutils\" ;;\n            pacman) pkg=\"bind\" ;;\n            apk | emerge) pkg=\"bind-tools\" ;;\n            yum | dnf | zypper) pkg=\"bind-utils\" ;;\n            esac\n            ;;\n        iconv)\n            case \"$pkg_mgr\" in\n            apk) pkg=\"musl-utils\" ;;\n            *) error_and_exit \"Which GNU/Linux do not have iconv built-in?\" ;;\n            esac\n            ;;\n        *) pkg=$cmd ;;\n        esac\n    }\n\n    # 系统                       package名称                                    repo名称\n    # centos/alma/rocky/fedora   epel-release                                   epel\n    # oracle linux               oracle-epel-release                            ol9_developer_EPEL\n    # opencloudos                epol-release                                   EPOL\n    # alibaba cloud linux 3      epel-release/epel-aliyuncs-release(qcow2自带)  epel\n    # anolis 23                  anolis-epao-release                            EPAO\n\n    # anolis 8\n    # [root@localhost ~]# yum search *ep*-release | grep -v next\n    # ========================== Name Matched: *ep*-release ==========================\n    # anolis-epao-release.noarch : EPAO Packages for Anolis OS 8 repository configuration\n    # epel-aliyuncs-release.noarch : Extra Packages for Enterprise Linux repository configuration\n    # epel-release.noarch : Extra Packages for Enterprise Linux repository configuration (qcow2自带)\n\n    check_is_need_epel() {\n        is_need_epel() {\n            case \"$pkg\" in\n            dpkg) true ;;\n            jq) is_have_cmd yum && ! is_have_cmd dnf ;; # el7/ol7 的 jq 在 epel 仓库\n            *) false ;;\n            esac\n        }\n\n        get_epel_repo_name() {\n            # el7 不支持 yum repolist --all，要使用 yum repolist all\n            # el7 yum repolist 第一栏有 /x86_64 后缀，因此要去掉。而 el9 没有\n            $pkg_mgr repolist all | awk '{print $1}' | awk -F/ '{print $1}' | grep -Ei 'ep(el|ol|ao)$'\n        }\n\n        get_epel_pkg_name() {\n            # el7 不支持 yum list --available，要使用 yum list available\n            $pkg_mgr list available | grep -E '(.*-)?ep(el|ol|ao)-(.*-)?release' |\n                awk '{print $1}' | cut -d. -f1 | grep -v next | head -1\n        }\n\n        if is_need_epel; then\n            if ! epel=$(get_epel_repo_name); then\n                $pkg_mgr install -y \"$(get_epel_pkg_name)\"\n                epel=$(get_epel_repo_name)\n            fi\n            enable_epel=\"--enablerepo=$epel\"\n        else\n            enable_epel=\n        fi\n    }\n\n    install_pkg_real() {\n        text=\"$pkg\"\n        if [ \"$pkg\" != \"$cmd\" ]; then\n            text+=\" ($cmd)\"\n        fi\n        echo \"Installing package '$text'...\"\n\n        case $pkg_mgr in\n        dnf)\n            check_is_need_epel\n            dnf install $enable_epel -y --setopt=install_weak_deps=False $pkg\n            ;;\n        yum)\n            check_is_need_epel\n            yum install $enable_epel -y $pkg\n            ;;\n        emerge) emerge --oneshot $pkg ;;\n        pacman) pacman -Syu --noconfirm --needed $pkg ;;\n        zypper) zypper install -y $pkg ;;\n        apk)\n            add_community_repo_for_alpine\n            apk add $pkg\n            ;;\n        apt-get)\n            [ -z \"$apt_updated\" ] && apt-get update && apt_updated=1\n            DEBIAN_FRONTEND=noninteractive apt-get install -y $pkg\n            ;;\n        nix-env)\n            # 不指定 channel 会很慢，而且很占内存\n            [ -z \"$nix_updated\" ] && nix-channel --update && nix_updated=1\n            nix-env -iA nixos.$pkg\n            ;;\n        esac\n    }\n\n    is_need_reinstall() {\n        cmd=$1\n\n        # gentoo 默认编译的 unsquashfs 不支持 xz\n        if [ \"$cmd\" = unsquashfs ] && is_have_cmd emerge && ! $cmd |& grep -wq xz; then\n            echo \"unsquashfs not supported xz. rebuilding.\"\n            return 0\n        fi\n\n        # busybox fdisk 无法显示 mbr 分区表的 id\n        if [ \"$cmd\" = fdisk ] && is_have_cmd apk && $cmd |& grep -wq BusyBox; then\n            return 0\n        fi\n\n        # busybox grep 不支持 -oP\n        if [ \"$cmd\" = grep ] && is_have_cmd apk && $cmd |& grep -wq BusyBox; then\n            return 0\n        fi\n\n        return 1\n    }\n\n    for cmd in \"$@\"; do\n        if ! is_have_cmd $cmd || is_need_reinstall $cmd; then\n            if ! find_pkg_mgr; then\n                error_and_exit \"Can't find compatible package manager. Please manually install $cmd.\"\n            fi\n            cmd_to_pkg\n            install_pkg_real\n        fi\n    done >&2\n}\n\nis_valid_ram_size() {\n    is_digit \"$1\" && [ \"$1\" -gt 0 ]\n}\n\ncheck_ram() {\n    ram_standard=$(\n        case \"$distro\" in\n        netboot.xyz) echo 0 ;;\n        alpine | debian | kali | dd) echo 256 ;;\n        arch | gentoo | aosc | nixos | windows) echo 512 ;;\n        redhat | centos | almalinux | rocky | fedora | oracle | ubuntu | anolis | opencloudos | openeuler) echo 1024 ;;\n        opensuse | fnos) echo -1 ;; # 没有安装模式\n        esac\n    )\n\n    # 不用检查内存的情况\n    if [ \"$ram_standard\" -eq 0 ]; then\n        return\n    fi\n\n    # 未测试\n    ram_cloud_image=256\n\n    has_cloud_image=$(\n        case \"$distro\" in\n        redhat | centos | almalinux | rocky | oracle | fedora | debian | ubuntu | opensuse | anolis | openeuler) echo true ;;\n        netboot.xyz | alpine | dd | arch | gentoo | nixos | kali | windows) echo false ;;\n        esac\n    )\n\n    if is_in_windows; then\n        ram_size=$(wmic memorychip get capacity | awk -F= '{sum+=$2} END {if(sum>0) print sum/1024/1024}')\n    else\n        # lsmem最准确但 centos7 arm 和 alpine 不能用，debian 9 util-linux 没有 lsmem\n        # arm 24g dmidecode 显示少了128m\n        # arm 24g lshw 显示23BiB\n        # ec2 t4g arm alpine 用 lsmem 和 dmidecode 都无效，要用 lshw，但结果和free -m一致，其他平台则没问题\n        install_pkg lsmem\n        ram_size=$(lsmem -b 2>/dev/null | grep 'Total online memory:' | awk '{ print $NF/1024/1024 }')\n\n        if ! is_valid_ram_size \"$ram_size\"; then\n            install_pkg dmidecode\n            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}')\n        fi\n\n        if ! is_valid_ram_size \"$ram_size\"; then\n            install_pkg lshw\n            # 不能忽略 -i，alpine 显示的是 System memory\n            ram_str=$(lshw -c memory -short | grep -i 'System Memory' | awk '{print $3}')\n            ram_size=$(grep <<<$ram_str -o '[0-9]*')\n            grep <<<$ram_str GiB && ram_size=$((ram_size * 1024))\n        fi\n    fi\n\n    # 用于兜底，不太准确\n    # cygwin 要装 procps-ng 才有 free 命令\n    if ! is_valid_ram_size \"$ram_size\"; then\n        ram_size_k=$(grep '^MemTotal:' /proc/meminfo | awk '{print $2}')\n        ram_size=$((ram_size_k / 1024 + 64 + 4))\n    fi\n\n    if ! is_valid_ram_size \"$ram_size\"; then\n        error_and_exit \"Could not detect RAM size.\"\n    fi\n\n    # ram 足够就用普通方法安装，否则如果内存大于512就用 cloud image\n    # TODO: 测试 256 384 内存\n    if ! is_use_cloud_image && [ $ram_size -lt $ram_standard ]; then\n        if $has_cloud_image; then\n            info \"RAM < $ram_standard MB. Fallback to cloud image mode\"\n            cloud_image=1\n        else\n            error_and_exit \"Could not install $distro: RAM < $ram_standard MB.\"\n        fi\n    fi\n\n    if is_use_cloud_image && [ $ram_size -lt $ram_cloud_image ]; then\n        error_and_exit \"Could not install $distro using cloud image: RAM < $ram_cloud_image MB.\"\n    fi\n}\n\nis_efi() {\n    if is_in_windows; then\n        # bcdedit | grep -qi '^path.*\\.efi'\n        mountvol | grep -q -a 'EFI'\n    else\n        [ -d /sys/firmware/efi ]\n    fi\n}\n\nis_grub_dir_linked() {\n    # cloudcone 重装前/重装后(方法1)\n    [ \"$(readlink -f /boot/grub/grub.cfg)\" = /boot/grub2/grub.cfg ] ||\n        [ \"$(readlink -f /boot/grub2/grub.cfg)\" = /boot/grub/grub.cfg ] ||\n        # cloudcone 重装后(方法2)\n        { [ -f /boot/grub2/grub.cfg ] && [ \"$(cat /boot/grub2/grub.cfg)\" = 'chainloader (hd0)+1' ]; }\n}\n\nis_secure_boot_enabled() {\n    if is_efi; then\n        if is_in_windows; then\n            reg query 'HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecureBoot\\State' /v UEFISecureBootEnabled 2>/dev/null | grep 0x1\n        else\n            if dmesg | grep -i 'Secure boot enabled'; then\n                return 0\n            fi\n            install_pkg mokutil\n            mokutil --sb-state 2>&1 | grep -i 'SecureBoot enabled'\n        fi\n    else\n        return 1\n    fi\n}\n\nis_need_boot_vmlinuz() {\n    ! { is_netboot_xyz && is_efi; }\n}\n\n# 只有 linux bios 是用本机的 grub/extlinux\nis_use_local_grub_extlinux() {\n    is_need_boot_vmlinuz && ! is_in_windows && ! is_efi\n}\n\nis_use_local_grub() {\n    is_use_local_grub_extlinux && is_mbr_using_grub\n}\n\nis_use_local_extlinux() {\n    is_use_local_grub_extlinux && ! is_mbr_using_grub\n}\n\n# 软 raid 时 xda 可能不是引导盘，以后再修正\nis_mbr_using_grub() {\n    find_main_disk\n    # 各发行版不一定自带 strings hexdump xxd od 命令\n    head -c 440 /dev/$xda | grep -a -iq 'GRUB'\n}\n\nto_upper() {\n    tr '[:lower:]' '[:upper:]'\n}\n\nto_lower() {\n    tr '[:upper:]' '[:lower:]'\n}\n\ndel_cr() {\n    # wmic/reg 换行符是 \\r\\r\\n\n    # wmic nicconfig where InterfaceIndex=$id get MACAddress,IPAddress,IPSubnet,DefaultIPGateway | hexdump -c\n    sed -E 's/\\r+$//'\n}\n\ndel_empty_lines() {\n    sed '/^[[:space:]]*$/d'\n}\n\ndel_comment_lines() {\n    sed '/^[[:space:]]*#/d'\n}\n\ntrim() {\n    # sed -E -e 's/^[[:space:]]+//' -e 's/[[:space:]]+$//'\n    sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'\n}\n\nprompt_password() {\n    info \"prompt password\"\n    warn false \"Leave blank to use a random password.\"\n    warn false \"不填写则使用随机密码\"\n    while true; do\n        IFS= read -r -p \"Password: \" password\n        if [ -n \"$password\" ]; then\n            IFS= read -r -p \"Retype password: \" password_confirm\n            if [ \"$password\" = \"$password_confirm\" ]; then\n                break\n            else\n                error \"Passwords don't match. Try again.\"\n            fi\n        else\n            # 特殊字符列表\n            # https://learn.microsoft.com/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/hh994562(v=ws.11)\n            # 有的机器运行 centos 7 ，用 /dev/random 产生 16 位密码，开启了 rngd 也要 5 秒，关闭了 rngd 则长期阻塞\n            chars=\\''A-Za-z0-9~!@#$%^&*_=+`|(){}[]:;\"<>,.?/-'\n            password=$(tr -dc \"$chars\" </dev/urandom | head -c16)\n            break\n        fi\n    done\n}\n\nsave_password() {\n    dir=$1\n\n    # mkpasswd 有三个\n    # expect 里的 mkpasswd 是用来生成随机密码的\n    # whois 里的 mkpasswd 才是我们想要的，可能不支持 yescrypt，alpine 的 mkpasswd 是独立的包\n    # busybox 里的 mkpasswd 也是我们想要的，但多数不支持 yescrypt\n\n    # alpine 这两个包有冲突\n    # apk add expect mkpasswd\n\n    # 不要用 echo \"$password\" 保存密码，原因：\n    # password=\"-n\"\n    # echo \"$password\"  # 空白\n\n    # 明文密码\n    # 假如用户运行 alpine live 直接打包硬盘镜像，如果保存了明文密码，则会暴露明文密码，因为 netboot initrd 在里面\n    # 通过 --password 传入密码，history 有记录，也会暴露明文密码\n    # /reinstall.log 也会暴露明文密码（已处理）\n    if false; then\n        printf '%s' \"$password\" >>\"$dir/password-plaintext\"\n    fi\n\n    # sha512\n    # 以下系统均支持 sha512 密码，但是生成密码需要不同的工具\n    # 兼容性     openssl   mkpasswd          busybox  python\n    # centos 7     ×      只有expect的       需要编译    √\n    # centos 8     √      只有expect的\n    # debian 9     ×         √\n    # ubuntu 16    ×         √\n    # alpine       √      可能系统装了expect     √\n    # cygwin       √\n    # others       √\n\n    # alpine\n    if is_have_cmd busybox && busybox mkpasswd --help 2>&1 | grep -wq sha512; then\n        crypted=$(printf '%s' \"$password\" | busybox mkpasswd -m sha512)\n    # others\n    elif install_pkg openssl && openssl passwd --help 2>&1 | grep -wq '\\-6'; then\n        crypted=$(printf '%s' \"$password\" | openssl passwd -6 -stdin)\n    # debian 9 / ubuntu 16\n    elif is_have_cmd apt-get && install_pkg whois && mkpasswd -m help | grep -wq sha-512; then\n        crypted=$(printf '%s' \"$password\" | mkpasswd -m sha-512 --stdin)\n    # centos 7\n    # crypt.mksalt 是 python3 的\n    # 红帽把它 backport 到了 centos7 的 python2 上\n    # 在其它发行版的 python2 上运行会出错\n    elif is_have_cmd yum && is_have_cmd python2; then\n        crypted=$(python2 -c \"import crypt, sys; print(crypt.crypt(sys.argv[1], crypt.mksalt(crypt.METHOD_SHA512)))\" \"$password\")\n    else\n        error_and_exit \"Could not generate sha512 password.\"\n    fi\n    echo \"$crypted\" >\"$dir/password-linux-sha512\"\n\n    # yescrypt\n    # 旧系统不支持，先不管\n    if false; then\n        if mkpasswd -m help | grep -wq yescrypt; then\n            crypted=$(printf '%s' \"$password\" | mkpasswd -m yescrypt --stdin)\n            echo \"$crypted\" >\"$dir/password-linux-yescrypt\"\n        fi\n    fi\n\n    # windows\n    if [ \"$distro\" = windows ] || [ \"$distro\" = dd ]; then\n        install_pkg iconv\n\n        # 要分两行写，因为 echo \"$(xxx)\" 返回值始终为 0，出错也不会中断脚本\n        # grep . 为了保证脚本没有出错\n        base64=$(printf '%s' \"${password}Password\" | iconv -f UTF-8 -t UTF-16LE | base64 -w 0 | grep .)\n        echo \"$base64\" >\"$dir/password-windows-user-base64\"\n\n        base64=$(printf '%s' \"${password}AdministratorPassword\" | iconv -f UTF-8 -t UTF-16LE | base64 -w 0 | grep .)\n        echo \"$base64\" >\"$dir/password-windows-administrator-base64\"\n    fi\n}\n\n# 记录主硬盘\nfind_main_disk() {\n    if [ -n \"$main_disk\" ]; then\n        return\n    fi\n\n    if is_in_windows; then\n        # TODO:\n        # 已测试 vista\n        # 测试 软raid\n        # 测试 动态磁盘\n\n        # diskpart 命令结果\n        # 磁盘 ID: E5FDE61C\n        # 磁盘 ID: {92CF6564-9B2E-4348-A3BD-D84E3507EBD7}\n        main_disk=$(printf \"%s\\n%s\" \"select volume $c\" \"uniqueid disk\" | diskpart |\n            tail -1 | awk '{print $NF}' | sed 's,[{}],,g')\n    else\n        if [ -z \"$xda\" ]; then\n            # centos7下测试     lsblk --inverse $mapper | grep -w disk     grub2-probe -t disk /\n            # 跨硬盘btrfs       只显示第一个硬盘                            显示两个硬盘\n            # 跨硬盘lvm         显示两个硬盘                                显示/dev/mapper/centos-root\n            # 跨硬盘软raid      显示两个硬盘                                显示/dev/md127\n\n            # 还有 findmnt\n\n            # 理论上 /boot/efi /efi /boot 可以不在主硬盘上\n            # 因此只查找 / 分区\n\n            install_pkg lsblk\n            # lvm 显示的是 /dev/mapper/xxx-yyy，再用第二条命令得到sda\n            mapper=$(mount | awk '$3==\"/\" {print $1}' | grep .)\n            xdas=$(lsblk -rn --inverse $mapper | grep -w disk | awk '{print $1}' | sort -u | grep .)\n\n            # 注意 wc -l 的坑\n            # wc -l <<<\"\" 输出 1\n\n            # 检测主硬盘是否横跨多个磁盘\n            if [ \"$(wc -l <<<\"$xdas\")\" -eq 1 ]; then\n                xda=$xdas\n            else\n                # vultr 官网新建数据盘时有写，部分地区的实例，不能从数据盘启动\n                # 实测 nvram 可写入数据盘的条目但不生效，手动安装 debian 到此硬盘，nvram 也不生效\n                # 从 bios 手动选择数据盘的 efi 文件才能启动\n                # 但是 nvram BootNext 数据盘的条目却可以生效\n\n                # vultr 从软 raid 0 debian 下用本脚本安装 debian ，进入 grub efi 时，无法识别系统分区 md1 的文件系统，也少了 hd1\n                # 但是从 bios 手动选择 reinstall 的 grub efi 时却可以识别\n                # 后期应该将 vmlinux/initrd 放到 efi/boot 分区\n\n                # Gemini 说:\n                # 使用 efibootmgr 的 BootNext 时，固件走的是“快速路径”：\n                # 它只初始化存放 EFI 引导文件（grubx64.efi）的那块硬盘（hd0），然后直接把控制权交给 GRUB。\n                # 因为 hd1 还没“醒”，GRUB 自然无法拼凑出完整的 md1 阵列，导致无法识别文件系统。\n\n                # 使用 nativedisk 可以强制 grub 识别所有硬盘?\n                info false \"Multiple disks found for root partition:\"\n                echo '-----'\n                printf \"%s\\n\" \"$xdas\"\n                echo '-----'\n                read -r -p \"Select a disk to install: \" xda\n                if ! grep -Fqx \"$xda\" <<<\"$xdas\"; then\n                    error_and_exit \"Invalid Input.\"\n                fi\n            fi\n        fi\n\n        info \"Main disk: $xda\"\n\n        # 可以用 dd 找出 guid?\n\n        # centos7 blkid lsblk 不显示 PTUUID\n        # centos7 sfdisk 不显示 Disk identifier\n        # alpine blkid 不显示 gpt 分区表的 PTUUID\n        # 因此用 fdisk\n\n        # Disk identifier: 0x36778223                                  # gnu fdisk + mbr\n        # Disk identifier: D6B17C1A-FA1E-40A1-BDCB-0278A3ED9CFC        # gnu fdisk + gpt\n        # Disk identifier (GUID): d6b17c1a-fa1e-40a1-bdcb-0278a3ed9cfc # busybox fdisk + gpt\n        # 不显示 Disk identifier                                        # busybox fdisk + mbr\n\n        # 获取 xda 的 id\n        install_pkg fdisk\n        main_disk=$(fdisk -l /dev/$xda | grep 'Disk identifier' | awk '{print $NF}' | sed 's/0x//')\n    fi\n\n    # 检查 id 格式是否正确\n    if ! grep -Eix '[0-9a-f]{8}' <<<\"$main_disk\" &&\n        ! grep -Eix '[0-9a-f-]{36}' <<<\"$main_disk\"; then\n        error_and_exit \"Disk ID is invalid: $main_disk\"\n    fi\n}\n\nis_found_ipv4_netconf() {\n    [ -n \"$ipv4_mac\" ] && [ -n \"$ipv4_addr\" ] && [ -n \"$ipv4_gateway\" ]\n}\n\nis_found_ipv6_netconf() {\n    [ -n \"$ipv6_mac\" ] && [ -n \"$ipv6_addr\" ] && [ -n \"$ipv6_gateway\" ]\n}\n\n# TODO: 单网卡多IP\ncollect_netconf() {\n    if is_in_windows; then\n        convert_net_str_to_array() {\n            config=$1\n            key=$2\n            var=$3\n            IFS=',' read -r -a \"${var?}\" <<<\"$(grep \"$key=\" <<<\"$config\" | cut -d= -f2 | sed 's/[{}\\\"]//g')\"\n        }\n\n        # 部分机器精简了 powershell\n        # 所以不要用 powershell 获取网络信息\n        # 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')\n\n        # 否        手动        0    0.0.0.0/0                  19  192.168.1.1\n        # 否        手动        0    0.0.0.0/0                  59  nekoray-tun\n\n        # wmic nic:\n        # 真实网卡\n        # AdapterType=以太网 802.3\n        # AdapterTypeId=0\n        # MACAddress=68:EC:C5:11:11:11\n        # PhysicalAdapter=TRUE\n        # PNPDeviceID=PCI\\VEN_8086&amp;DEV_095A&amp;SUBSYS_94108086&amp;REV_61\\4&amp;295A4BD&amp;1&amp;00E0\n\n        # VPN tun 网卡，部分移动云电脑也有\n        # AdapterType=\n        # AdapterTypeId=\n        # MACAddress=\n        # PhysicalAdapter=TRUE\n        # PNPDeviceID=SWD\\WINTUN\\{6A460D48-FB76-6C3F-A47D-EF97D3DC6B0E}\n\n        # VMware 网卡\n        # AdapterType=以太网 802.3\n        # AdapterTypeId=0\n        # MACAddress=00:50:56:C0:00:08\n        # PhysicalAdapter=TRUE\n        # PNPDeviceID=ROOT\\VMWARE\\0001\n\n        for v in 4 6; do\n            if [ \"$v\" = 4 ]; then\n                # 或者 route print\n                routes=$(netsh int ipv4 show route | awk '$4 == \"0.0.0.0/0\"')\n            else\n                routes=$(netsh int ipv6 show route | awk '$4 == \"::/0\"')\n            fi\n\n            if [ -z \"$routes\" ]; then\n                continue\n            fi\n\n            while read -r route; do\n                if false; then\n                    read -r _ _ _ _ id gateway <<<\"$route\"\n                else\n                    id=$(awk '{print $5}' <<<\"$route\")\n                    gateway=$(awk '{print $6}' <<<\"$route\")\n                fi\n\n                config=$(wmic nicconfig where InterfaceIndex=$id get MACAddress,IPAddress,IPSubnet,DefaultIPGateway)\n                # 排除 IP/子网/网关/MAC 为空的\n                if grep -q '=$' <<<\"$config\"; then\n                    continue\n                fi\n\n                mac_addr=$(grep \"MACAddress=\" <<<\"$config\" | cut -d= -f2 | to_lower)\n                convert_net_str_to_array \"$config\" IPAddress ips\n                convert_net_str_to_array \"$config\" IPSubnet subnets\n                convert_net_str_to_array \"$config\" DefaultIPGateway gateways\n\n                # IPv4\n                # shellcheck disable=SC2154\n                if [ \"$v\" = 4 ]; then\n                    for ((i = 0; i < ${#ips[@]}; i++)); do\n                        ip=${ips[i]}\n                        subnet=${subnets[i]}\n                        if [[ \"$ip\" = *.* ]]; then\n                            # ipcalc 依赖 perl，会使 cygwin 增加 ~50M\n                            # cidr=$(ipcalc -b \"$ip/$subnet\" | grep Netmask: | awk '{print $NF}')\n                            cidr=$(mask2cidr \"$subnet\")\n                            ipv4_addr=\"$ip/$cidr\"\n                            ipv4_gateway=\"$gateway\"\n                            ipv4_mac=\"$mac_addr\"\n                            # 只取第一个 IP\n                            break\n                        fi\n                    done\n                fi\n\n                # IPv6\n                if [ \"$v\" = 6 ]; then\n                    ipv6_type_list=$(netsh interface ipv6 show address $id normal)\n                    for ((i = 0; i < ${#ips[@]}; i++)); do\n                        ip=${ips[i]}\n                        cidr=${subnets[i]}\n                        if [[ \"$ip\" = *:* ]]; then\n                            ipv6_type=$(grep \"$ip\" <<<\"$ipv6_type_list\" | awk '{print $1}')\n                            # Public 是 slaac\n                            # 还有类型 Temporary，不过有 Temporary 肯定还有 Public，因此不用\n                            if [ \"$ipv6_type\" = Public ] ||\n                                [ \"$ipv6_type\" = Dhcp ] ||\n                                [ \"$ipv6_type\" = Manual ]; then\n                                ipv6_addr=\"$ip/$cidr\"\n                                ipv6_gateway=\"$gateway\"\n                                ipv6_mac=\"$mac_addr\"\n                                # 只取第一个 IP\n                                break\n                            fi\n                        fi\n                    done\n                fi\n\n                # 网关\n                # shellcheck disable=SC2154\n                if false; then\n                    for gateway in \"${gateways[@]}\"; do\n                        if [ -n \"$ipv4_addr\" ] && [[ \"$gateway\" = *.* ]]; then\n                            ipv4_gateway=\"$gateway\"\n                        elif [ -n \"$ipv6_addr\" ] && [[ \"$gateway\" = *:* ]]; then\n                            ipv6_gateway=\"$gateway\"\n                        fi\n                    done\n                fi\n\n                # 如果通过本条 route 的网卡找到了 IP 则退出 routes 循环\n                if is_found_ipv${v}_netconf; then\n                    break\n                fi\n            done < <(echo \"$routes\")\n        done\n    else\n        # linux\n        # 通过默认网关得到默认网卡\n\n        # 多个默认路由下\n        # ip -6 route show default dev ens3 完全不显示\n\n        # ip -6 route show default\n        # default proto static metric 1024 pref medium\n        #         nexthop via 2a01:1111:262:4940::2 dev ens3 weight 1 onlink\n        #         nexthop via fe80::5054:ff:fed4:5286 dev ens3 weight 1\n\n        # ip -6 route show default\n        # default via 2602:1111:0:80::1 dev eth0 metric 1024 onlink pref medium\n\n        # arch + vultr\n        # ip -6 route show default\n        # default nhid 4011550343 via fe80::fc00:5ff:fe3d:2714 dev enp1s0 proto ra metric 1024 expires 1504sec pref medium\n\n        for v in 4 6; do\n            if via_gateway_dev_ethx=$(ip -$v route show default | grep -Ewo 'via [^ ]+ dev [^ ]+' | head -1 | grep .); then\n                read -r _ gateway _ ethx <<<\"$via_gateway_dev_ethx\"\n                eval ipv${v}_ethx=\"$ethx\" # can_use_cloud_kernel 要用\n                eval ipv${v}_mac=\"$(ip link show dev $ethx | grep link/ether | head -1 | awk '{print $2}')\"\n                eval ipv${v}_gateway=\"$gateway\"\n\n                # 获取所有全局地址\n                all_addrs=$(ip -$v -o addr show scope global dev $ethx | grep -v temporary | awk '{print $4}')\n                primary_addr=$(echo \"$all_addrs\" | head -1)\n\n                # IPv6: 用 ip route get 让内核返回正确的源 IP，指定 dev 避免 tun/warp 干扰\n                if [ \"$v\" = 6 ] && [ -n \"$primary_addr\" ]; then\n                    route_src=$(ip -6 route get 2001:4860:4860::8888 dev \"$ethx\" 2>/dev/null | grep -oP 'src \\K[^ ]+')\n                    if [ -n \"$route_src\" ]; then\n                        for addr in $all_addrs; do\n                            if [ \"${addr%/*}\" = \"$route_src\" ]; then\n                                primary_addr=$addr\n                                break\n                            fi\n                        done\n                    fi\n                fi\n\n                eval ipv${v}_addr=\"$primary_addr\"\n                # extra_addrs: 除主地址外的所有地址\n                eval ipv${v}_extra_addrs=\"$(echo \"$all_addrs\" | grep -Fxve \"$primary_addr\" | tr '\\n' ',' | sed 's/,$//')\"\n            fi\n        done\n    fi\n\n    if ! is_found_ipv4_netconf && ! is_found_ipv6_netconf; then\n        error_and_exit \"Can not get IP info.\"\n    fi\n\n    info \"Network Info\"\n    echo \"IPv4 MAC: $ipv4_mac\"\n    echo \"IPv4 Address: $ipv4_addr\"\n    echo \"IPv4 Gateway: $ipv4_gateway\"\n    echo \"---\"\n    echo \"IPv6 MAC: $ipv6_mac\"\n    echo \"IPv6 Address: $ipv6_addr\"\n    echo \"IPv6 Gateway: $ipv6_gateway\"\n    echo\n}\n\nget_efi_dir_in_windows() {\n    # 挂载\n    if result=$(find /cygdrive/?/EFI/Microsoft/Boot/bootmgfw.efi 2>/dev/null); then\n        # 已经挂载\n        x=$(echo $result | cut -d/ -f3)\n    else\n        # 找到空盘符并挂载\n        for x in {a..z}; do\n            [ ! -e /cygdrive/$x ] && break\n        done\n        if ! mountvol $x: /s >&2; then\n            error_and_exit \"Can't mount efi partition in windows.\"\n        fi\n    fi\n    echo \"/cygdrive/$x\"\n}\n\nadd_efi_entry_in_windows() {\n    info \"Add efi entry in windows\"\n\n    local source=$1\n\n    # 文件夹命名为reinstall而不是grub，因为可能机器已经安装了grub，bcdedit名字同理\n    dist_dir=\"$(get_efi_dir_in_windows)/EFI/reinstall\"\n    efi_drive=$(echo \"$dist_dir\" | cut -d/ -f3)\n    basename=$(basename $source)\n    download_or_copy_file \"$source\" \"$dist_dir/$basename\"\n\n    # 如果 {fwbootmgr} displayorder 为空\n    # 执行 bcdedit /copy '{bootmgr}' 会报错\n    # 例如 azure windows 2016 模板\n    # 要先设置默认的 {fwbootmgr} displayorder\n    # https://github.com/hakuna-m/wubiuefi/issues/286\n    bcdedit /set '{fwbootmgr}' displayorder '{bootmgr}' /addfirst\n\n    # 添加启动项\n    id=$(bcdedit /copy '{bootmgr}' /d \"$(get_entry_name)\" | grep -o '{.*}')\n    bcdedit /set $id device partition=$efi_drive:\n    bcdedit /set $id path \\\\EFI\\\\reinstall\\\\$basename\n    bcdedit /set '{fwbootmgr}' bootsequence $id\n}\n\nget_maybe_efi_dirs_in_linux() {\n    # 不从 fstab 查找，因为极端情况下可能只用 systemd mount\n    # arch云镜像efi分区挂载在/efi，且使用 autofs，mount 命令会有两个 /efi 条目\n\n    install_pkg findmnt >&2\n\n    # 寻找 efi 分区，并输出根目录\n    # root_dirs=$(mount | awk '$5==\"vfat\" || $5==\"autofs\" {print $3}' | grep -Ex '/efi|/boot/efi|/boot' | sort -u)\n    root_dirs=$(findmnt -t fat,vfat -n -o TARGET | grep -Ex '/efi|/boot/efi|/boot' | sort -u)\n\n    efi_dirs=$(\n        for dir in $root_dirs; do\n            # 只显示有 efi 文件的\n            # -quit 表示找到第一个 *.efi 就立即退出 find\n            if [ -d \"$dir\" ]; then\n                find \"$dir\" -type f -iname \"*.efi\" -exec printf '%s\\n' \"$dir\" \\; -quit\n            fi\n        done\n    )\n\n    if [ -z \"$efi_dirs\" ]; then\n        error_and_exit \"Can't find efi partition.\"\n    fi\n\n    echo \"$efi_dirs\"\n}\n\nget_disk_by_part() {\n    dev_part=$1\n    install_pkg lsblk >&2\n    lsblk -rn --inverse \"$dev_part\" | grep -w disk | awk '{print $1}'\n}\n\nget_part_num_by_part() {\n    dev_part=$1\n    grep -oE '[0-9]*$' <<<\"$dev_part\"\n}\n\ngrep_efi_entry() {\n    # efibootmgr\n    # BootCurrent: 0002\n    # Timeout: 1 seconds\n    # BootOrder: 0000,0002,0003,0001\n    # Boot0000* sles-secureboot\n    # Boot0001* CD/DVD Rom\n    # Boot0002* Hard Disk\n    # Boot0003* sles-secureboot\n    # MirroredPercentageAbove4G: 0.00\n    # MirrorMemoryBelow4GB: false\n\n    # 根据文档，* 表示 active，也就是说有可能没有*(代表inactive)\n    # https://manpages.debian.org/testing/efibootmgr/efibootmgr.8.en.html\n    grep -E '^Boot[0-9a-fA-F]{4}'\n}\n\n# trans.sh 有同名方法\ngrep_efi_index() {\n    awk '{print $1}' | sed -e 's/Boot//' -e 's/\\*//'\n}\n\ndownload_or_copy_file() {\n    local source=$1\n    local dist=$2\n\n    mkdir -p \"$(dirname $dist)\"\n\n    if [[ \"$source\" = http* ]]; then\n        curl -Lo \"$dist\" \"$source\"\n    else\n        cp -f \"$source\" \"$dist\"\n    fi\n}\n\nadd_efi_entry_in_linux() {\n    local source=$1\n\n    info \"Add efi entry in linux\"\n\n    install_pkg efibootmgr\n\n    # 只取第一个\n    # 因为不用关心是否为 efi 分区，只要是 fat/vfat 格式的分区即可添加到引导\n    # 这里用了管道导致 get_maybe_efi_dirs_in_linux 里面的 error_and_exit 不生效\n    efi_part=$(get_maybe_efi_dirs_in_linux | head -1 | grep .)\n    dist_dir=$efi_part/EFI/reinstall\n    basename=$(basename $source)\n    download_or_copy_file \"$source\" \"$dist_dir/$basename\"\n\n    # 原系统可能不是用 grub 引导，因此不一定有 grub-probe\n    if false; then\n        grub_probe=\"$(command -v grub-probe grub2-probe | head -1)\"\n        dev_part=\"$(\"$grub_probe\" -t device \"$dist_dir\")\"\n    else\n        install_pkg findmnt\n        # arch findmnt 会得到\n        # systemd-1\n        # /dev/sda2\n        dev_part=$(findmnt -T \"$dist_dir\" -no SOURCE | grep '^/dev/')\n    fi\n\n    if ! {\n        res=$(efibootmgr --create-only \\\n            --disk \"/dev/$(get_disk_by_part $dev_part)\" \\\n            --part \"$(get_part_num_by_part $dev_part)\" \\\n            --label \"$(get_entry_name)\" \\\n            --loader \"\\\\EFI\\\\reinstall\\\\$basename\") &&\n            id=$(echo \"$res\" | grep_efi_entry | tail -1 | grep_efi_index | grep .) &&\n            efibootmgr --bootnext \"$id\"\n    }; then\n        echo \"$res\"\n        error_and_exit \"Could not add efi entry.\"\n    fi\n}\n\nget_grub_efi_filename() {\n    case \"$basearch\" in\n    x86_64) echo grubx64.efi ;;\n    aarch64) echo grubaa64.efi ;;\n    esac\n}\n\ninstall_grub_linux_efi() {\n    info 'download grub efi'\n\n    # fedora 39 的 efi 无法识别 opensuse tumbleweed 的 xfs\n    efi_distro=fedora\n\n    grub_efi=$(get_grub_efi_filename)\n\n    # 不要用 download.opensuse.org 和 download.fedoraproject.org\n    # 因为 ipv6 访问有时跳转到 ipv4 地址，造成 ipv6 only 机器无法下载\n    # 日韩机器有时得到国内镜像源，但镜像源屏蔽了国外 IP 导致连不上\n    # https://mirrors.bfsu.edu.cn/opensuse/ports/aarch64/tumbleweed/repo/oss/EFI/BOOT/grub.efi\n\n    # fcix 经常 404\n    # https://mirror.fcix.net/opensuse/tumbleweed/repo/oss/EFI/BOOT/bootx64.efi\n    # https://mirror.fcix.net/opensuse/tumbleweed/appliances/openSUSE-Tumbleweed-Minimal-VM.x86_64-Cloud.qcow2\n\n    # dl.fedoraproject.org 不支持 ipv6\n\n    if [ \"$efi_distro\" = fedora ]; then\n        # fedora 43 efi 在 vultr 无法引导 debain 9/10 netboot\n        fedora_ver=$(get_latest_distro_releasever fedora)\n\n        if is_in_china; then\n            mirror=https://mirror.nju.edu.cn/fedora\n        else\n            mirror=https://d2lzkl7pfhq30w.cloudfront.net/pub/fedora/linux\n        fi\n\n        curl -Lo $tmp/$grub_efi $mirror/releases/$fedora_ver/Everything/$basearch/os/EFI/BOOT/$grub_efi\n    else\n        if is_in_china; then\n            mirror=https://mirror.nju.edu.cn/opensuse\n        else\n            mirror=https://downloadcontentcdn.opensuse.org\n        fi\n\n        [ \"$basearch\" = x86_64 ] && ports='' || ports=/ports/$basearch\n\n        curl -Lo $tmp/$grub_efi $mirror$ports/tumbleweed/repo/oss/EFI/BOOT/grub.efi\n    fi\n\n    add_efi_entry_in_linux $tmp/$grub_efi\n}\n\ndownload_and_extract_apk() {\n    local alpine_ver=$1\n    local package=$2\n    local extract_dir=$3\n\n    install_pkg tar xz\n    is_in_china && mirror=http://mirror.nju.edu.cn/alpine || mirror=https://dl-cdn.alpinelinux.org/alpine\n    package_apk=$(curl -L $mirror/v$alpine_ver/main/$basearch/ | grep -oP \"$package-[^-]*-[^-]*\\.apk\" | sort -u)\n    if ! [ \"$(wc -l <<<\"$package_apk\")\" -eq 1 ]; then\n        error_and_exit \"find no/multi apks.\"\n    fi\n    mkdir -p \"$extract_dir\"\n\n    # 屏蔽警告\n    tar 2>&1 | grep -q BusyBox && tar_args= || tar_args=--warning=no-unknown-keyword\n    curl -L \"$mirror/v$alpine_ver/main/$basearch/$package_apk\" | tar xz $tar_args -C \"$extract_dir\"\n}\n\ninstall_grub_win() {\n    # 下载 grub\n    info download grub\n\n    # arm64 模块要单独下载，要注意版本匹配\n    grub_ver=2.06\n\n    # ftpmirror.gnu.org 是 geoip 重定向，不是 cdn\n    # 有可能重定义到一个拉黑了部分 IP 的服务器\n\n    # 换成 ftp.gnu.org?\n    is_in_china && grub_url=https://mirror.nju.edu.cn/gnu/grub/grub-$grub_ver-for-windows.zip ||\n        grub_url=https://mirrors.kernel.org/gnu/grub/grub-$grub_ver-for-windows.zip\n    curl -Lo $tmp/grub.zip $grub_url\n    # unzip -qo $tmp/grub.zip\n    7z x $tmp/grub.zip -o$tmp -r -y -xr!i386-efi -xr!locale -xr!themes -bso0\n    grub_dir=$tmp/grub-$grub_ver-for-windows\n    grub=$grub_dir/grub\n\n    # 设置 grub 包含的模块\n    # 原系统是 windows，因此不需要 ext2 lvm xfs btrfs\n    grub_modules+=\" normal minicmd serial ls echo test cat reboot halt linux chain search all_video configfile\"\n    grub_modules+=\" scsi part_msdos part_gpt fat ntfs ntfscomp lzopio xzio gzio zstd\"\n    if ! is_efi; then\n        grub_modules+=\" biosdisk linux16\"\n    fi\n\n    # 设置 grub prefix 为c盘根目录\n    # 运行 grub-probe 会改变cmd窗口字体\n    prefix=$($grub-probe -t drive $c: | sed 's|.*PhysicalDrive|(hd|' | del_cr)/\n    echo $prefix\n\n    # 安装 grub\n    if is_efi; then\n        # efi\n        info install grub for efi\n\n        case \"$basearch\" in\n        x86_64) grub_arch=x86_64 ;;\n        aarch64) grub_arch=arm64 ;;\n        esac\n\n        # 下载 grub arm64 模块\n        # 注意要匹配 grub-for-windows 版本\n        if ! [ -d $grub_dir/grub/$grub_arch-efi ]; then\n            # 3.20 是 grub 2.12，可能会有问题\n            alpine_ver=3.19\n            download_and_extract_apk $alpine_ver grub-efi $tmp/grub-efi\n            cp -r $tmp/grub-efi/usr/lib/grub/$grub_arch-efi/ $grub_dir\n        fi\n\n        grub_efi=$(get_grub_efi_filename)\n        $grub-mkimage -p $prefix -O $grub_arch-efi -o \"$(cygpath -w \"$grub_dir/$grub_efi\")\" $grub_modules\n        add_efi_entry_in_windows \"$grub_dir/$grub_efi\"\n    else\n        # bios\n        info install grub for bios\n\n        # bootmgr 加载 g2ldr 有大小限制\n        # 超过大小会报错 0xc000007b\n        # 解决方法1 g2ldr.mbr + g2ldr\n        # 解决方法2 生成少于64K的 g2ldr + 动态模块\n        if false; then\n            # g2ldr.mbr\n            # 部分国内机无法访问 ftp.cn.debian.org\n            is_in_china && host=mirror.nju.edu.cn || host=deb.debian.org\n            curl -LO http://$host/debian/tools/win32-loader/oldstable/win32-loader.exe\n            7z x win32-loader.exe 'g2ldr.mbr' -o$tmp/win32-loader -r -y -bso0\n            find $tmp/win32-loader -name 'g2ldr.mbr' -exec cp {} /cygdrive/$c/ \\;\n\n            # g2ldr\n            # 配置文件 c:\\grub.cfg\n            $grub-mkimage -p \"$prefix\" -O i386-pc -o \"$(cygpath -w $grub_dir/core.img)\" $grub_modules\n            cat $grub_dir/i386-pc/lnxboot.img $grub_dir/core.img >/cygdrive/$c/g2ldr\n        else\n            # grub-install 无法设置 prefix\n            # 配置文件 c:\\grub\\grub.cfg\n            $grub-install $c \\\n                --target=i386-pc \\\n                --boot-directory=$c: \\\n                --install-modules=\"$grub_modules\" \\\n                --themes= \\\n                --fonts= \\\n                --no-bootsector\n\n            cat $grub_dir/i386-pc/lnxboot.img /cygdrive/$c/grub/i386-pc/core.img >/cygdrive/$c/g2ldr\n        fi\n\n        # 添加引导\n        # 脚本可能不是首次运行，所以先删除原来的\n        id='{1c41f649-1637-52f1-aea8-f96bfebeecc8}'\n        bcdedit /enum all | grep -a $id && bcdedit /delete $id\n        bcdedit /create $id /d \"$(get_entry_name)\" /application bootsector\n        bcdedit /set $id device partition=$c:\n        bcdedit /set $id path \\\\g2ldr\n        bcdedit /displayorder $id /addlast\n        bcdedit /bootsequence $id /addfirst\n    fi\n}\n\nfind_grub_extlinux_cfg() {\n    dir=$1\n    filename=$2\n    keyword=$3\n\n    # 当 ln -s /boot/grub /boot/grub2 时\n    # find /boot/ 会自动忽略 /boot/grub2 里面的文件\n    cfgs=$(\n        # 只要 $dir 存在\n        # 无论是否找到结果，返回值都是 0\n        find $dir \\\n            -type f -name $filename \\\n            -exec grep -E -l \"$keyword\" {} \\;\n    )\n\n    count=\"$(wc -l <<<\"$cfgs\")\"\n    if [ \"$count\" -eq 1 ]; then\n        echo \"$cfgs\"\n    else\n        error_and_exit \"Find $count $filename.\"\n    fi\n}\n\n# 空格、&、用户输入的网址要加引号，否则 grub 无法正确识别\nis_need_quote() {\n    [[ \"$1\" = *' '* ]] || [[ \"$1\" = *'&'* ]] || [[ \"$1\" = http* ]]\n}\n\n# 转换 finalos_a=1 为 finalos.a=1 ，排除 finalos_mirrorlist\nbuild_finalos_cmdline() {\n    if vars=$(compgen -v finalos_); then\n        for key in $vars; do\n            value=${!key}\n            key=${key#finalos_}\n            if [ -n \"$value\" ] && [ $key != \"mirrorlist\" ]; then\n                is_need_quote \"$value\" &&\n                    finalos_cmdline+=\" finalos_$key='$value'\" ||\n                    finalos_cmdline+=\" finalos_$key=$value\"\n            fi\n        done\n    fi\n}\n\nbuild_extra_cmdline() {\n    # 使用 extra_xxx=yyy 而不是 extra.xxx=yyy\n    # 因为 debian installer /lib/debian-installer-startup.d/S02module-params\n    # 会将 extra.xxx=yyy 写入新系统的 /etc/modprobe.d/local.conf\n    # https://answers.launchpad.net/ubuntu/+question/249456\n    # https://salsa.debian.org/installer-team/rootskel/-/blob/master/src/lib/debian-installer-startup.d/S02module-params?ref_type=heads\n    for key in confhome hold force_boot_mode force_cn force_old_windows_setup cloud_image main_disk \\\n        elts deb_mirror \\\n        ssh_port rdp_port web_port allow_ping; do\n        value=${!key}\n        if [ -n \"$value\" ]; then\n            is_need_quote \"$value\" &&\n                extra_cmdline+=\" extra_$key='$value'\" ||\n                extra_cmdline+=\" extra_$key=$value\"\n        fi\n    done\n\n    # 指定最终安装系统的 mirrorlist，链接有&，在grub中是特殊字符，所以要加引号\n    if [ -n \"$finalos_mirrorlist\" ]; then\n        extra_cmdline+=\" extra_mirrorlist='$finalos_mirrorlist'\"\n    elif [ -n \"$nextos_mirrorlist\" ]; then\n        extra_cmdline+=\" extra_mirrorlist='$nextos_mirrorlist'\"\n    fi\n\n    # cloudcone 特殊处理\n    if is_grub_dir_linked; then\n        finalos_cmdline+=\" extra_link_grub_dir=1\"\n    fi\n}\n\necho_tmp_ttys() {\n    if false; then\n        curl -L $confhome/ttys.sh | sh -s \"console=\"\n    else\n        case \"$basearch\" in\n        x86_64) echo \"console=ttyS0,115200n8 console=tty0\" ;;\n        aarch64) echo \"console=ttyS0,115200n8 console=ttyAMA0,115200n8 console=tty0\" ;;\n        esac\n    fi\n}\n\nget_entry_name() {\n    printf 'reinstall ('\n    printf '%s' \"$distro\"\n    [ -n \"$releasever\" ] && printf ' %s' \"$releasever\"\n    [ \"$distro\" = alpine ] && [ \"$hold\" = 1 ] && printf ' Live OS'\n    printf ')'\n}\n\n# shellcheck disable=SC2154\nbuild_nextos_cmdline() {\n    if [ $nextos_distro = alpine ]; then\n        nextos_cmdline=\"alpine_repo=$nextos_repo modloop=$nextos_modloop\"\n    elif is_distro_like_debian $nextos_distro; then\n        # 设置分辨率为800*600，防止分辨率过高 ssh screen attach 后无法全部显示\n        # iso 默认有 vga=788\n        # 如果要设置位数: video=800x600-16\n        nextos_cmdline=\"lowmem/low=1 auto=true priority=critical\"\n        # nextos_cmdline+=\" vga=788 video=800x600\"\n        nextos_cmdline+=\" url=$nextos_ks\"\n        nextos_cmdline+=\" mirror/http/hostname=${nextos_udeb_mirror%/*}\"\n        nextos_cmdline+=\" mirror/http/directory=/${nextos_udeb_mirror##*/}\"\n        nextos_cmdline+=\" base-installer/kernel/image=$nextos_kernel\"\n        # elts 的 debian 不能用 security 源，否则安装过程会提示无法访问\n        if [ \"$nextos_distro\" = debian ] && is_debian_elts; then\n            nextos_cmdline+=\" apt-setup/services-select=\"\n        fi\n        # kali 安装好后网卡是 eth0 这种格式，但安装时不是\n        if [ \"$nextos_distro\" = kali ]; then\n            nextos_cmdline+=\" net.ifnames=0\"\n            nextos_cmdline+=\" simple-cdd/profiles=kali\"\n        fi\n    elif is_distro_like_redhat $nextos_distro; then\n        # redhat\n        nextos_cmdline=\"root=live:$nextos_squashfs inst.ks=$nextos_ks\"\n    fi\n\n    if is_distro_like_debian $nextos_distro; then\n        if [ \"$basearch\" = \"x86_64\" ]; then\n            # debian installer 好像第一个 tty 是主 tty\n            # 设置ttyS0,tty0,安装界面还是显示在ttyS0\n            :\n        else\n            # debian arm 在没有ttyAMA0的机器上（aws t4g），最少要设置一个tty才能启动\n            # 只设置tty0也行，但安装过程ttyS0没有显示\n            nextos_cmdline+=\" $(echo_tmp_ttys)\"\n        fi\n    else\n        nextos_cmdline+=\" $(echo_tmp_ttys)\"\n    fi\n    # nextos_cmdline+=\" mem=256M\"\n    # nextos_cmdline+=\" lowmem=+1\"\n}\n\nbuild_cmdline() {\n    # nextos\n    build_nextos_cmdline\n\n    # finalos\n    # trans 需要 finalos_distro 识别是安装 alpine 还是其他系统\n    if [ \"$distro\" = alpine ]; then\n        finalos_distro=alpine\n    fi\n    if [ -n \"$finalos_distro\" ]; then\n        build_finalos_cmdline\n    fi\n\n    # extra\n    build_extra_cmdline\n\n    cmdline=\"$nextos_cmdline $finalos_cmdline $extra_cmdline\"\n}\n\n# 脚本可能多次运行，先清理之前的残留\nmkdir_clear() {\n    dir=$1\n\n    if [ -z \"$dir\" ] || [ \"$dir\" = / ]; then\n        return\n    fi\n\n    # 再次运行时，有可能 mount 了 btrfs root，因此先要 umount_all\n    # 但目前不需要 mount ，因此用不到\n    # umount_all \"$dir\"\n    rm -rf \"$dir\"\n    mkdir -p \"$dir\"\n}\n\nmod_initrd_debian_kali() {\n    # hack 1\n    # 允许设置 ipv4 onlink 网关\n    sed -Ei 's,&&( onlink=),||\\1,' etc/udhcpc/default.script\n\n    # hack 2\n    # 强制使用 screen\n    # shellcheck disable=SC1003,SC2016\n    {\n        echo 'if false && : \\' | insert_into_file lib/debian-installer.d/S70menu before 'if [ -x \"$bterm\" ]' -F\n        echo 'if true  || : \\' | insert_into_file lib/debian-installer.d/S70menu before 'if [ -x \"$screen_bin\" -a' -F\n    }\n\n    # hack 3\n    # 修改 /var/lib/dpkg/info/netcfg.postinst 运行我们的脚本\n    netcfg() {\n        #!/bin/sh\n        # shellcheck source=/dev/null\n        . /usr/share/debconf/confmodule\n        db_progress START 0 5 debian-installer/netcfg/title\n\n        : get_ip_conf_cmd\n\n        # 运行 trans.sh，保存配置\n        db_progress INFO base-installer/progress/netcfg\n        # 添加 || exit ，可以在 debian installer 不兼容 /trans.sh 语法时强制报错\n        # exit 不带参数，返回值为 || 前面命令的返回值\n        sh /trans.sh || exit\n        db_progress STEP 1\n        db_progress STOP\n    }\n\n    postinst=var/lib/dpkg/info/netcfg.postinst\n    get_function_content netcfg >$postinst\n    get_ip_conf_cmd | insert_into_file $postinst after \": get_ip_conf_cmd\"\n    # cat $postinst\n\n    # hack 4\n    # 修改 udeb 依赖\n\n    # 直接覆盖 net-retriever，方便调试\n    # curl -Lo /usr/lib/debian-installer/retriever/net-retriever $confhome/net-retriever\n\n    change_priority() {\n        while IFS= read -r line; do\n            if [[ \"$line\" = Package:* ]]; then\n                package=$(echo \"$line\" | cut -d' ' -f2-)\n\n            elif [[ \"$line\" = Priority:* ]]; then\n                # shellcheck disable=SC2154\n                if [ \"$line\" = \"Priority: standard\" ]; then\n                    for p in $disabled_list; do\n                        if [ \"$package\" = \"$p\" ]; then\n                            line=\"Priority: optional\"\n                            break\n                        fi\n                    done\n                elif [[ \"$package\" = ata-modules* ]]; then\n                    # 改成强制安装\n                    # 因为是 pata-modules sata-modules scsi-modules 的依赖\n                    # 但我们没安装它们，也就不会自动安装 ata-modules\n                    line=\"Priority: standard\"\n                fi\n            fi\n            echo \"$line\"\n        done\n    }\n\n    # shellcheck disable=SC2012\n    kver=$(ls -d lib/modules/* | awk -F/ '{print $NF}')\n\n    net_retriever=usr/lib/debian-installer/retriever/net-retriever\n    # shellcheck disable=SC2016\n    sed -i 's,>> \"$1\",| change_priority >> \"$1\",' $net_retriever\n    insert_into_file $net_retriever after '#!/bin/sh' <<EOF\ndisabled_list=\"\ndepthcharge-tools-installer\nkickseed-common\nnobootloader\npartman-btrfs\npartman-cros\npartman-iscsi\npartman-jfs\npartman-md\npartman-xfs\nrescue-check\nwpasupplicant-udeb\nlilo-installer\nsystemd-boot-installer\nnic-modules-$kver-di\nnic-pcmcia-modules-$kver-di\nnic-usb-modules-$kver-di\nnic-wireless-modules-$kver-di\nnic-shared-modules-$kver-di\npcmcia-modules-$kver-di\npcmcia-storage-modules-$kver-di\ncdrom-core-modules-$kver-di\nfirewire-core-modules-$kver-di\nusb-storage-modules-$kver-di\nisofs-modules-$kver-di\njfs-modules-$kver-di\nxfs-modules-$kver-di\nloop-modules-$kver-di\npata-modules-$kver-di\nsata-modules-$kver-di\nscsi-modules-$kver-di\n\"\n\n$(get_function change_priority)\nEOF\n\n    # https://github.com/linuxhw/LsPCI?tab=readme-ov-file#storageata-pci\n    # https://debian.pkgs.org/12/debian-main-amd64/linux-image-6.1.0-18-cloud-amd64_6.1.76-1_amd64.deb.html\n    # https://deb.debian.org/debian/pool/main/l/linux-signed-amd64/\n    # https://deb.debian.org/debian/dists/bookworm/main/debian-installer/binary-all/Packages.xz\n    # https://deb.debian.org/debian/dists/bookworm/main/debian-installer/binary-amd64/Packages.xz\n    # 以下是 debian-installer 有的驱动，这些驱动云内核不一定都有，(+)表示云内核有\n    # scsi-core-modules 默认安装（不用修改），是 ata-modules 的依赖\n    #                   包含 sd_mod.ko(+) scsi_mod.ko(+) scsi_transport_fc.ko(+) scsi_transport_sas.ko(+) scsi_transport_spi.ko(+)\n    # ata-modules       默认可选（改成必装），是下方模块的依赖。只有 ata_generic.ko(+) 和 libata.ko(+) 两个驱动\n\n    # pata-modules      默认安装（改成可选），里面的驱动都是 pata_ 开头，但只有 pata_legacy.ko(+) 在云内核中\n    # sata-modules      默认安装（改成可选），里面的驱动大部分是 sata_ 开头的，其他重要的还有 ahci.ko libahci.ko ata_piix.ko(+)\n    #                   云内核没有 sata 模块，也没有内嵌，有一个 CONFIG_SATA_HOST=y，libata-$(CONFIG_SATA_HOST)\t+= libata-sata.o\n    # scsi-modules      默认安装（改成可选），包含 nvme.ko(+) 和各种虚拟化驱动(+)\n\n    download_and_extract_deb() {\n        local type=$1\n        local package=$2\n        local extract_dir=$3\n\n        # shellcheck disable=SC2154\n        case \"$type\" in\n        deb)\n            local mirror=$nextos_deb_mirror\n            local url=http://$mirror/dists/$nextos_codename/main/binary-$basearch_alt/Packages.gz\n            ;;\n        udeb)\n            local mirror=$nextos_udeb_mirror\n            local url=http://$mirror/dists/$nextos_codename/main/debian-installer/binary-$basearch_alt/Packages.gz\n            ;;\n        esac\n\n        # 获取 deb/udeb 列表\n        deb_list=$tmp/${type}_list\n        if ! [ -f $deb_list ]; then\n            curl -L \"$url\" | zcat | grep 'Filename:' | awk '{print $2}' >$deb_list\n        fi\n\n        # 下载 deb/udeb\n        deb_path=$(grep -F \"/${package}_\" \"$deb_list\")\n        curl -Lo $tmp/tmp.deb http://$mirror/\"$deb_path\"\n\n        if false; then\n            # 使用 dpkg\n            # cygwin 没有 dpkg\n            install_pkg dpkg\n            dpkg -x $tmp/tmp.deb $extract_dir\n        else\n            # 使用 ar tar xz\n            # cygwin 需安装 binutils\n            # centos7 ar 不支持 --output\n            install_pkg ar tar xz\n            (cd $tmp && ar x $tmp/tmp.deb)\n            tar xf $tmp/data.tar.xz -C $extract_dir\n        fi\n    }\n\n    # 不用在 windows 判断是哪种硬盘控制器，因为 256M 运行 windows 只可能是 xp，而脚本本来就不支持 xp\n    # 在 debian installer 中判断能否用云内核\n    create_can_use_cloud_kernel_sh can_use_cloud_kernel.sh\n\n    # 下载 fix-eth-name 脚本\n    curl -LO \"$confhome/fix-eth-name.sh\"\n    curl -LO \"$confhome/fix-eth-name.service\"\n\n    # 有段时间 kali initrd 删除了原版 wget\n    # 但 initrd 的 busybox wget 又不支持 https\n    # 因此改成在这里下载\n    curl -LO \"$confhome/get-xda.sh\"\n    curl -LO \"$confhome/ttys.sh\"\n    if [ -n \"$frpc_config\" ]; then\n        curl -LO \"$confhome/get-frpc-url.sh\"\n        curl -LO \"$confhome/frpc.service\"\n    fi\n\n    # 可以节省一点内存？\n    echo 'export DEBCONF_DROP_TRANSLATIONS=1' |\n        insert_into_file lib/debian-installer/menu before 'exec debconf'\n\n    # 还原 kali netinst.iso 的 simple-cdd 机制\n    # 主要用于调用 kali.postinst 设置 zsh 为默认 shell\n    # 但 mini.iso 又没有这种机制\n    # https://gitlab.com/kalilinux/build-scripts/kali-live/-/raw/main/kali-config/common/includes.installer/kali-finish-install?ref_type=heads\n    # https://salsa.debian.org/debian/simple-cdd/-/blob/master/debian/14simple-cdd?ref_type=heads\n    # https://http.kali.org/pool/main/s/simple-cdd/simple-cdd-profiles_0.6.9_all.udeb\n    if [ \"$distro\" = kali ]; then\n        # 但我们没有使用 iso，因此没有 kali.postinst，需要另外下载\n        mkdir -p cdrom/simple-cdd\n        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\n        chmod a+x cdrom/simple-cdd/kali.postinst\n    fi\n\n    if [ \"$distro\" = debian ] && is_debian_elts; then\n        curl -Lo usr/share/keyrings/debian-archive-keyring.gpg https://deb.freexian.com/extended-lts/archive-key.gpg\n    fi\n\n    # 提前下载 sshd\n    # 以便在配置下载源之前就可以启动 sshd\n    mkdir_clear $tmp/sshd\n    download_and_extract_deb udeb openssh-server-udeb $tmp/sshd\n    cp -r $tmp/sshd/* .\n\n    # 提前下载 fdisk\n    # 因为 fdisk-udeb 包含 fdisk 和 sfdisk，提前下载可减少占用\n    mkdir_clear $tmp/fdisk\n    download_and_extract_deb udeb fdisk-udeb $tmp/fdisk\n    cp -f $tmp/fdisk/usr/sbin/fdisk usr/sbin/\n\n    # 下载 websocketd\n    # debian 11+ 才有 websocketd\n    if [ \"$distro\" = kali ] ||\n        { [ \"$distro\" = debian ] && [ \"$releasever\" -ge 11 ]; }; then\n        mkdir_clear $tmp/websocketd\n        download_and_extract_deb deb websocketd $tmp/websocketd\n        cp -f $tmp/websocketd/usr/bin/websocketd usr/bin/\n    fi\n\n    # >256M 或者当前系统是 windows\n    if [ $ram_size -gt 256 ] || is_in_windows; then\n        sed -i '/^pata-modules/d' $net_retriever\n        sed -i '/^sata-modules/d' $net_retriever\n        sed -i '/^scsi-modules/d' $net_retriever\n    else\n        # <=256M 极限优化\n        find_main_disk\n        extra_drivers=\n        for driver in $(get_disk_drivers $xda); do\n            echo \"using driver: $driver\"\n            case $driver in\n            nvme) extra_drivers+=\" nvme nvme-core\" ;;\n                # xen 的横杠特别不同\n            xen_blkfront) extra_drivers+=\" xen-blkfront\" ;;\n            xen_scsifront) extra_drivers+=\" xen-scsifront\" ;;\n            virtio_blk | virtio_scsi | hv_storvsc | vmw_pvscsi) extra_drivers+=\" $driver\" ;;\n            pata_legacy) sed -i '/^pata-modules/d' $net_retriever ;; # 属于 pata-modules\n            ata_piix) sed -i '/^sata-modules/d' $net_retriever ;;    # 属于 sata-modules\n            ata_generic) ;;                                          # 属于 ata-modules，不用处理，因为我们设置强制安装了 ata-modules\n            esac\n        done\n\n        # extra drivers\n        # xen 还需要以下两个？\n        # kernel/drivers/xen/xen-scsiback.ko\n        # kernel/drivers/block/xen-blkback/xen-blkback.ko\n        # 但反查也找不到 curl https://deb.debian.org/debian/dists/bookworm/main/Contents-udeb-amd64.gz | zcat | grep xen\n        if [ -n \"$extra_drivers\" ]; then\n            mkdir_clear $tmp/scsi\n            download_and_extract_deb udeb scsi-modules-$kver-di $tmp/scsi\n            relative_drivers_dir=lib/modules/$kver/kernel/drivers\n\n            udeb_drivers_dir=$tmp/scsi/$relative_drivers_dir\n            dist_drivers_dir=$initrd_dir/$relative_drivers_dir\n            (\n                cd $udeb_drivers_dir\n                for driver in $extra_drivers; do\n                    # debian 模块没有压缩\n                    # kali 模块有压缩\n                    # 因此要有 *\n                    if ! find $dist_drivers_dir -name \"$driver.ko*\" | grep -q .; then\n                        echo \"adding driver: $driver\"\n                        file=$(find . -name \"$driver.ko*\" | grep .)\n                        cp -fv --parents \"$file\" \"$dist_drivers_dir\"\n                    fi\n                done\n            )\n        fi\n    fi\n\n    # amd64)\n    # \tlevel1=737 # MT=754108, qemu: -m 780\n    # \tlevel2=424 # MT=433340, qemu: -m 460\n    # \tmin=316    # MT=322748, qemu: -m 350\n\n    # 将 use_level 2 9 修改为 use_level 1\n    # x86 use_level 2 会出现 No root file system is defined.\n    # arm 即使 use_level 1 也会出现 No root file system is defined.\n    sed -i 's/use_level=[29]/use_level=1/' lib/debian-installer-startup.d/S15lowmem\n\n    # hack 3\n    # 修改 trans.sh\n    # 1. 直接调用 create_ifupdown_config\n    # shellcheck disable=SC2154\n    insert_into_file $initrd_dir/trans.sh after '^: main' <<EOF\n        distro=$nextos_distro\n        releasever=$nextos_releasever\n        create_ifupdown_config /etc/network/interfaces\n        exit\nEOF\n    # 2. 删除 debian busybox 无法识别的语法\n    # 3. 删除 apk 语句\n    # 4. debian 11/12 initrd 无法识别 > >\n    # 5. debian 11/12 initrd 无法识别 < <\n    # 6. debian 11 initrd 无法识别 set -E\n    # 7. debian 11 initrd 无法识别 trap ERR\n    # 8. debian 9 initrd 无法识别 ${string//find/replace}\n    # 9. debian 12 initrd 无法识别 . <(\n    # 删除或注释，可能会导致空方法而报错，因此改为替换成'\\n: #'\n    replace='\\n: #'\n    sed -Ei \\\n        -e \"s/> >/$replace/\" \\\n        -e \"s/< </$replace/\" \\\n        -e \"s/\\. <\\(/$replace/\" \\\n        -e \"s/^[[:space:]]*apk[[:space:]]/$replace/\" \\\n        -e \"s/^[[:space:]]*trap[[:space:]]/$replace/\" \\\n        -e \"s/\\\\$\\{.*\\/\\/.*\\/.*\\}/$replace/\" \\\n        -e \"/^[[:space:]]*set[[:space:]]/s/E//\" \\\n        $initrd_dir/trans.sh\n}\n\nget_disk_drivers() {\n    get_drivers \"/sys/block/$1\"\n}\n\nget_net_drivers() {\n    get_drivers \"/sys/class/net/$1\"\n}\n\n# 不用在 windows 判断是哪种硬盘/网络驱动，因为 256M 运行 windows 只可能是 xp，而脚本本来就不支持 xp\n# 而且安装过程也有二次判断\nget_drivers() {\n    # 有以下结果组合出现\n    # sd_mod\n    # virtio_blk\n    # virtio_scsi\n    # virtio_pci\n    # pcieport\n    # xen_blkfront\n    # ahci\n    # nvme\n    # mptspi\n    # mptsas\n    # vmw_pvscsi\n    (\n        cd \"$(readlink -f $1)\"\n        while ! [ \"$(pwd)\" = / ]; do\n            if [ -d driver ]; then\n                if [ -d driver/module ]; then\n                    # 显示全名，例如 xen_blkfront sd_mod\n                    # 但 ahci 没有这个文件，所以 else 不能省略\n                    basename \"$(readlink -f driver/module)\"\n                else\n                    # 不显示全名，例如 vbd sd\n                    basename \"$(readlink -f driver)\"\n                fi\n            fi\n            cd ..\n        done\n    )\n}\n\nexit_if_cant_use_cloud_kernel() {\n    find_main_disk\n    collect_netconf\n\n    # shellcheck disable=SC2154\n    if ! can_use_cloud_kernel \"$xda\" $ipv4_ethx $ipv6_ethx; then\n        error_and_exit \"Can't use cloud kernel. And not enough RAM to run normal kernel.\"\n    fi\n}\n\ncan_use_cloud_kernel() {\n    # initrd 下也要使用，不要用 <<<\n\n    # 有些虚拟机用了 ahci，但云内核没有 ahci 驱动\n    cloud_eth_modules='ena|gve|mana|virtio_net|xen_netfront|hv_netvsc|vmxnet3|mlx4_en|mlx4_core|mlx5_core|ixgbevf'\n    cloud_blk_modules='ata_generic|ata_piix|pata_legacy|nvme|virtio_blk|virtio_scsi|xen_blkfront|xen_scsifront|hv_storvsc|vmw_pvscsi'\n\n    # disk\n    drivers=\"$(get_disk_drivers $1)\"\n    shift\n    for driver in $drivers; do\n        echo \"using disk driver: $driver\"\n    done\n    echo \"$drivers\" | grep -Ewq \"$cloud_blk_modules\" || return 1\n\n    # net\n    # v4 v6 eth 相同，只检查一次\n    if [ \"$1\" = \"$2\" ]; then\n        shift\n    fi\n    while [ $# -gt 0 ]; do\n        drivers=\"$(get_net_drivers $1)\"\n        shift\n        for driver in $drivers; do\n            echo \"using net driver: $driver\"\n        done\n        echo \"$drivers\" | grep -Ewq \"$cloud_eth_modules\" || return 1\n    done\n}\n\ncreate_can_use_cloud_kernel_sh() {\n    cat <<EOF >$1\n        $(get_function get_drivers)\n        $(get_function get_net_drivers)\n        $(get_function get_disk_drivers)\n        $(get_function can_use_cloud_kernel)\n\n        can_use_cloud_kernel \"\\$@\"\nEOF\n}\n\nget_ip_conf_cmd() {\n    collect_netconf >&2\n    is_in_china && is_in_china=true || is_in_china=false\n\n    sh=/initrd-network.sh\n    if is_found_ipv4_netconf && is_found_ipv6_netconf && [ \"$ipv4_mac\" = \"$ipv6_mac\" ]; then\n        echo \"'$sh' '$ipv4_mac' '$ipv4_addr' '$ipv4_gateway' '$ipv6_addr' '$ipv6_gateway' '$is_in_china' '$ipv6_extra_addrs'\"\n    else\n        if is_found_ipv4_netconf; then\n            echo \"'$sh' '$ipv4_mac' '$ipv4_addr' '$ipv4_gateway' '' '' '$is_in_china' ''\"\n        fi\n        if is_found_ipv6_netconf; then\n            echo \"'$sh' '$ipv6_mac' '' '' '$ipv6_addr' '$ipv6_gateway' '$is_in_china' '$ipv6_extra_addrs'\"\n        fi\n    fi\n}\n\nmod_initrd_alpine() {\n    # hack 1 v3.19 和之前的 virt 内核需添加 ipv6 模块\n    if virt_dir=$(ls -d $initrd_dir/lib/modules/*-virt 2>/dev/null); then\n        ipv6_dir=$virt_dir/kernel/net/ipv6\n        if ! [ -f $ipv6_dir/ipv6.ko ] && ! grep -q ipv6 $initrd_dir/lib/modules/*/modules.builtin; then\n            mkdir -p $ipv6_dir\n            modloop_file=$tmp/modloop_file\n            modloop_dir=$tmp/modloop_dir\n            curl -Lo $modloop_file $nextos_modloop\n            if is_in_windows; then\n                # cygwin 没有 unsquashfs\n                7z e $modloop_file ipv6.ko -r -y -o$ipv6_dir\n            else\n                install_pkg unsquashfs\n                mkdir_clear $modloop_dir\n                unsquashfs -f -d $modloop_dir $modloop_file 'modules/*/kernel/net/ipv6/ipv6.ko'\n                find $modloop_dir -name ipv6.ko -exec cp {} $ipv6_dir/ \\;\n            fi\n        fi\n    fi\n\n    # hack 下载 dhcpcd\n    # shellcheck disable=SC2154\n    download_and_extract_apk \"$nextos_releasever\" dhcpcd \"$initrd_dir\"\n    sed -i -e '/^slaac private/s/^/#/' -e '/^#slaac hwaddr/s/^#//' $initrd_dir/etc/dhcpcd.conf\n\n    # hack 2 /usr/share/udhcpc/default.script\n    # 脚本被调用的顺序\n    # udhcpc:  deconfig\n    # udhcpc:  bound\n    # udhcpc6: deconfig\n    # udhcpc6: bound\n    # shellcheck disable=SC2329\n    udhcpc() {\n        if [ \"$1\" = deconfig ]; then\n            return\n        fi\n        if [ \"$1\" = bound ] && [ -n \"$ipv6\" ]; then\n            # shellcheck disable=SC2154\n            ip -6 addr add \"$ipv6\" dev \"$interface\"\n            ip link set dev \"$interface\" up\n            return\n        fi\n    }\n\n    get_function_content udhcpc |\n        insert_into_file usr/share/udhcpc/default.script after 'deconfig\\|renew\\|bound'\n\n    # 允许设置 ipv4 onlink 网关\n    sed -Ei 's,(0\\.0\\.0\\.0\\/0),\"\\1 onlink\",' usr/share/udhcpc/default.script\n\n    # hack 3 网络配置\n    # alpine 根据 MAC_ADDRESS 判断是否有网络\n    # https://github.com/alpinelinux/mkinitfs/blob/c4c0115f9aa5aa8884c923dc795b2638711bdf5c/initramfs-init.in#L914\n    insert_into_file init after 'configure_ip\\(\\)' <<EOF\n        depmod\n        [ -d /sys/module/ipv6 ] || modprobe ipv6\n        $(get_ip_conf_cmd)\n        MAC_ADDRESS=1\n        return\nEOF\n\n    # grep -E -A5 'configure_ip\\(\\)' init\n\n    # hack 4 运行 trans.start\n    # 1. alpine arm initramfs 时间问题 要添加 --no-check-certificate\n    # 2. aws t4g arm 如果没设置console=ttyx，在initramfs里面wget https会出现bad header错误，chroot后正常\n    # Connecting to raw.githubusercontent.com (185.199.108.133:443)\n    # 60C0BB2FFAFF0000:error:0A00009C:SSL routines:ssl3_get_record:http request:ssl/record/ssl3_record.c:345:\n    # ssl_client: SSL_connect\n    # wget: bad header line: �\n    insert_into_file init before '^exec switch_root' <<EOF\n        # trans\n        # echo \"wget --no-check-certificate -O- $confhome/trans.sh | /bin/ash\" >\\$sysroot/etc/local.d/trans.start\n        # wget --no-check-certificate -O \\$sysroot/etc/local.d/trans.start $confhome/trans.sh\n        cp /trans.sh \\$sysroot/etc/local.d/trans.start\n        chmod a+x \\$sysroot/etc/local.d/trans.start\n        ln -s /etc/init.d/local \\$sysroot/etc/runlevels/default/\n\n        # 配置 + 自定义驱动\n        for dir in /configs /custom_drivers; do\n            if [ -d \\$dir ]; then\n                cp -r \\$dir \\$sysroot/\n                rm -rf \\$dir\n            fi\n        done\nEOF\n\n    # 判断云镜像 debain 能否用云内核\n    if is_distro_like_debian; then\n        create_can_use_cloud_kernel_sh can_use_cloud_kernel.sh\n        insert_into_file init before '^exec (/bin/busybox )?switch_root' <<EOF\n        cp /can_use_cloud_kernel.sh \\$sysroot/\n        chmod a+x \\$sysroot/can_use_cloud_kernel.sh\nEOF\n    fi\n}\n\nmod_initrd() {\n    info \"mod $nextos_distro initrd\"\n    install_pkg gzip cpio\n\n    # 解压\n    # 先删除临时文件，避免之前运行中断有残留文件\n    initrd_dir=$tmp/initrd\n    mkdir_clear $initrd_dir\n    cd $initrd_dir\n\n    # cygwin 下处理 debian initrd 时\n    # 解压/重新打包/删除 initrd 的 /dev/console /dev/null 都会报错\n    # cpio: dev/console: Cannot utime: Invalid argument\n    # cpio: ./dev/console: Cannot stat: Bad address\n    # 用 windows 文件管理器可删除\n\n    # 但同样运行 zcat /reinstall-initrd | cpio -idm\n    # 打开 C:\\cygwin\\Cygwin.bat ，运行报错\n    # 打开桌面的 Cygwin 图标，运行就没问题\n\n    # shellcheck disable=SC2046\n    # nonmatching 是精确匹配路径\n    zcat /reinstall-initrd | cpio -idm \\\n        $(is_in_windows && echo --nonmatching 'dev/console' --nonmatching 'dev/null')\n\n    curl -Lo $initrd_dir/trans.sh $confhome/trans.sh\n    if ! grep -iq \"$SCRIPT_VERSION\" $initrd_dir/trans.sh; then\n        error_and_exit \"\nThis script is outdated, please download reinstall.sh again.\n脚本有更新，请重新下载 reinstall.sh\"\n    fi\n\n    curl -Lo $initrd_dir/initrd-network.sh $confhome/initrd-network.sh\n    chmod a+x $initrd_dir/trans.sh $initrd_dir/initrd-network.sh\n\n    # 保存配置\n    mkdir -p $initrd_dir/configs\n    if [ -n \"$ssh_keys\" ]; then\n        cat <<<\"$ssh_keys\" >$initrd_dir/configs/ssh_keys\n    else\n        save_password $initrd_dir/configs\n    fi\n    if [ -n \"$frpc_config\" ]; then\n        cat \"$frpc_config\" >$initrd_dir/configs/frpc.conf\n    fi\n\n    # 收集 cloud-data 打包进 initrd\n    if [ -n \"$cloud_data\" ]; then\n        mkdir -p $initrd_dir/configs/cloud-data\n        if [ -d \"$cloud_data\" ]; then\n            # 本地目录：直接复制\n            cp \"$cloud_data\"/* $initrd_dir/configs/cloud-data/\n        else\n            # URL：在 host 下载\n            for f in user-data meta-data network-config; do\n                curl -fsSL \"$cloud_data/$f\" -o \"$initrd_dir/configs/cloud-data/$f\" 2>/dev/null || true\n            done\n        fi\n        # 校验：至少要有 user-data\n        [ -f $initrd_dir/configs/cloud-data/user-data ] || error_and_exit \"--cloud-data must contain user-data\"\n        cloud_data_files=$(ls $initrd_dir/configs/cloud-data/ | tr '\\n' ' ')\n    fi\n\n    if is_distro_like_debian $nextos_distro; then\n        mod_initrd_debian_kali\n    else\n        mod_initrd_$nextos_distro\n    fi\n\n    # 添加自定义 windows 驱动\n    if [ \"$distro\" = windows ] && [ -n \"$custom_infs\" ]; then\n        # shellcheck disable=SC1090\n        . <(curl -L $confhome/windows-driver-utils.sh)\n        echo \"$custom_infs\" | while read -r inf; do\n            parse_inf_and_cp_driever \"$inf\" \"$initrd_dir/custom_drivers\" \"$basearch_alt\" true\n        done\n    fi\n\n    # alpine live 不精简 initrd\n    # 因为不知道用户想干什么，可能会用到精简的文件\n    if is_virt && ! is_alpine_live; then\n        remove_useless_initrd_files\n    fi\n\n    if [ \"$hold\" = 0 ]; then\n        info 'hold 0'\n        read -r -p 'Press Enter to continue...'\n    fi\n\n    # 重建\n    # 注意要用 cpio -H newc 不要用 cpio -c ，不同版本的 -c 作用不一样，很坑\n    # -c    Use the old portable (ASCII) archive format\n    # -c    Identical to \"-H newc\", use the new (SVR4)\n    #       portable format.If you wish the old portable\n    #       (ASCII) archive format, use \"-H odc\" instead.\n    find . | cpio --quiet -o -H newc -R 0:0 | gzip -1 >/reinstall-initrd\n    cd - >/dev/null\n}\n\nremove_useless_initrd_files() {\n    info \"slim initrd\"\n\n    # 显示精简前的大小\n    du -sh .\n\n    # 删除 initrd 里面没用的文件/驱动\n    rm -rf bin/brltty\n    rm -rf etc/brltty\n    rm -rf sbin/wpa_supplicant\n    rm -rf usr/lib/libasound.so.*\n    rm -rf usr/share/alsa\n    (\n        cd lib/modules/*/kernel/drivers/net/ethernet/\n        for item in *; do\n            case \"$item\" in\n            # 甲骨文 arm 用自定义镜像支持设为 mlx5 vf 网卡，且不是 azure 那样显示两个网卡\n            # https://debian.pkgs.org/13/debian-main-amd64/linux-image-6.12.43+deb13-cloud-amd64_6.12.43-1_amd64.deb.html\n            amazon | google | mellanox | realtek | pensando) ;;\n            intel)\n                (\n                    cd \"$item\"\n                    for sub_item in *; do\n                        case \"$sub_item\" in\n                        # 有 e100.ko e1000文件夹 e1000e文件夹\n                        e100* | lib* | *vf | idpf) ;;\n                        *) rm -rf $sub_item ;;\n                        esac\n                    done\n                )\n                ;;\n            *) rm -rf $item ;;\n            esac\n        done\n    )\n    (\n        cd lib/modules/*/kernel\n        for item in \\\n            net/mac80211 \\\n            net/wireless \\\n            net/bluetooth \\\n            drivers/hid \\\n            drivers/mmc \\\n            drivers/mtd \\\n            drivers/usb \\\n            drivers/ssb \\\n            drivers/mfd \\\n            drivers/bcma \\\n            drivers/pcmcia \\\n            drivers/parport \\\n            drivers/platform \\\n            drivers/staging \\\n            drivers/net/usb \\\n            drivers/net/bonding \\\n            drivers/net/wireless \\\n            drivers/input/rmi4 \\\n            drivers/input/keyboard \\\n            drivers/input/touchscreen \\\n            drivers/bus/mhi \\\n            drivers/char/pcmcia \\\n            drivers/misc/cardreader; do\n            rm -rf $item\n        done\n    )\n\n    # 显示精简后的大小\n    du -sh .\n}\n\nget_unix_path() {\n    if is_in_windows; then\n        # 输入的路径是 / 开头也没问题\n        cygpath -u \"$1\"\n    else\n        printf '%s' \"$1\"\n    fi\n}\n\ninit_basearch() {\n    # 设置 basearch\n    if is_in_windows; then\n        # x86-based PC\n        # x64-based PC\n        # ARM-based PC\n        # ARM64-based PC\n\n        # 三种方法都不需要管理员运行\n        if false; then\n            # 如果机器没有 wmic 则需要下载 wmic.ps1，但此时未判断国内外，还是用国外源\n            basearch=$(wmic ComputerSystem get SystemType | grep '=' | cut -d= -f2 | cut -d- -f1)\n        elif true; then\n            basearch=$(reg query \"HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\" /v PROCESSOR_ARCHITECTURE |\n                grep . | tail -1 | awk '{print $NF}')\n        else\n            # 也可以用\n            basearch=$(cmd /c \"if defined PROCESSOR_ARCHITEW6432 (echo %PROCESSOR_ARCHITEW6432%) else (echo %PROCESSOR_ARCHITECTURE%)\")\n        fi\n    else\n        # archlinux 云镜像没有 arch 命令\n        # https://en.wikipedia.org/wiki/Uname\n        basearch=$(uname -m)\n    fi\n\n    # 统一架构名称，并强制 64 位\n    case \"$(echo $basearch | to_lower)\" in\n    i?86 | x64 | x86* | amd64)\n        basearch=x86_64\n        basearch_alt=amd64\n        ;;\n    arm* | aarch64)\n        basearch=aarch64\n        basearch_alt=arm64\n        ;;\n    *) error_and_exit \"Unsupported arch: $basearch\" ;;\n    esac\n}\n\ninit_confhome() {\n    # 设置 confhome\n    # 未测试\n    if false && [[ \"$confhome\" = http*://raw.githubusercontent.com/* ]]; then\n        repo=$(echo $confhome | cut -d/ -f4,5)\n        branch=$(echo $confhome | cut -d/ -f6)\n        # 避免脚本更新时，文件不同步造成错误\n        if [ -z \"$commit\" ]; then\n            commit=$(curl -L https://api.github.com/repos/$repo/git/refs/heads/$branch |\n                grep '\"sha\"' | grep -Eo '[0-9a-f]{40}')\n        fi\n        # shellcheck disable=SC2001\n        confhome=$(echo \"$confhome\" | sed \"s/main$/$commit/\")\n    fi\n\n    # 设置国内代理\n    # 要在使用 wmic 前设置，否则国内机器会从国外源下载 wmic.ps1\n    # gitee 不支持ipv6\n    # jsdelivr 有12小时缓存\n    # https://github.com/XIU2/UserScript/blob/master/GithubEnhanced-High-Speed-Download.user.js#L31\n    if is_in_china; then\n        if [ -n \"$confhome_cn\" ]; then\n            confhome=$confhome_cn\n        elif [ -n \"$github_proxy\" ] && [[ \"$confhome\" = http*://raw.githubusercontent.com/* ]]; then\n            confhome=${confhome/http:\\/\\//https:\\/\\/}\n            confhome=${confhome/https:\\/\\/raw.githubusercontent.com/$github_proxy}\n        fi\n    fi\n}\n\nremove_exist_reinstall_efi_dir() {\n    info \"remove exist reinstall efi dir\"\n\n    local dir='' dirs=''\n    if is_in_windows; then\n        dirs=$(get_efi_dir_in_windows)\n    else\n        dirs=$(get_maybe_efi_dirs_in_linux)\n    fi\n    # 后期可能会将 reinstall-vmlinuz 和 reinstall-initrd 放到 efi 分区下\n    # 因此也删除它们\n    for dir in $dirs; do\n        rm -f \"$dir/reinstall-vmlinuz\"\n        rm -f \"$dir/reinstall-initrd\"\n    done\n    find $dirs -type f \\\n        \\( -ipath '*/EFI/reinstall/grubx64.efi' \\\n        -o -ipath '*/EFI/reinstall/grubaa64.efi' \\\n        -o -ipath '*/EFI/reinstall/netboot.xyz.efi' \\\n        -o -ipath '*/EFI/reinstall/netboot.xyz-arm64.efi' \\) |\n        while IFS= read -r efi_file; do\n            reinstall_dir=$(dirname \"$efi_file\")\n            echo \"removing $reinstall_dir\"\n            rm -rf \"$reinstall_dir\"\n        done\n}\n\n#             linux                                      windows\n# bios        /boot/grub*/custom.cfg                     /cygdrive/c/grub/grub.cfg\n# efi         efi分区的/EFI/reinstall/grub.cfg           /cygdrive/c/grub.cfg\n# efi文件夹    efi分区的/EFI/reinstall/                  /cygdrive/a/EFI/reinstall/\n\ninit_bootloader_facts() {\n    if is_in_windows; then\n        # windows\n        if is_efi; then\n            _grub_cfg=/cygdrive/$c/grub.cfg\n        else\n            _grub_cfg=/cygdrive/$c/grub/grub.cfg\n        fi\n        target_cfg=$_grub_cfg\n    else\n        # linux\n        if is_efi; then\n            efi_dir=$(get_maybe_efi_dirs_in_linux | head -1)\n            _grub_cfg=$efi_dir/EFI/reinstall/grub.cfg\n            target_cfg=$_grub_cfg\n        else\n            if is_mbr_using_grub; then\n                if is_have_cmd update-grub; then\n                    # alpine debian ubuntu\n                    _grub_cfg=$(grep -o '[^ ]*grub.cfg' \"$(get_cmd_path update-grub)\" | head -1)\n                else\n                    # 找出主配置文件（含有menuentry|blscfg）\n                    # 有没有可能在 efi 目录?\n                    _grub_cfg=$(find_grub_extlinux_cfg '/boot/grub*' grub.cfg 'menuentry|blscfg')\n                fi\n                target_cfg=$(dirname $_grub_cfg)/custom.cfg\n\n                if is_have_cmd grub2-mkconfig; then\n                    grub=grub2\n                elif is_have_cmd grub-mkconfig; then\n                    grub=grub\n                else\n                    error_and_exit \"grub not found\"\n                fi\n            else\n                # extlinux\n                _extlinux_cfg=$(find_grub_extlinux_cfg /boot extlinux.conf LINUX)\n                target_cfg=$_extlinux_cfg\n            fi\n        fi\n    fi\n}\n\n# 重新生成 grub.cfg\n# 因为有些机子例如hython debian的grub.cfg少了40_custom 41_custom 部分\nrecreate_grub_or_extlinux_cfg() {\n    # 没用到原机的 grub 和 extlinux\n    # 因此不需要重新生成 grub.cfg 或 extlinux.conf\n    if is_efi || is_in_windows; then\n        return\n    fi\n\n    if is_mbr_using_grub; then\n        info \"recreate grub.cfg\"\n\n        # nixos 手动执行 grub-mkconfig -o /boot/grub/grub.cfg 会丢失系统启动条目\n        # 正确的方法是修改 configuration.nix 的 boot.loader.grub.extraEntries\n        # 但是修改 configuration.nix 不是很好，因此改成修改 grub.cfg\n        if [ -x /nix/var/nix/profiles/system/bin/switch-to-configuration ]; then\n            # 生成 grub.cfg\n            /nix/var/nix/profiles/system/bin/switch-to-configuration boot\n            # 手动启用 41_custom\n            nixos_grub_home=\"$(dirname \"$(readlink -f \"$(get_cmd_path grub-mkconfig)\")\")/..\"\n            $nixos_grub_home/etc/grub.d/41_custom >>$target_cfg\n        elif is_have_cmd update-grub; then\n            update-grub\n        else\n            $grub-mkconfig -o $target_cfg\n        fi\n    elif is_have_cmd update-extlinux; then\n        # alpine 才有 update-extlinux\n        info \"recreate extlinux.conf\"\n        update-extlinux\n    else\n        error_and_exit \"unsupported bootloader.\"\n    fi\n}\n\n# 删除之前的 reinstall 启动项\nremove_exist_reinstall() {\n    info \"remove exist reinstall\"\n\n    rm -f /reinstall-vmlinuz /reinstall-initrd\n    rm -f /boot/reinstall-vmlinuz /boot/reinstall-initrd\n    if is_in_windows; then\n        rm -f /cygdrive/$c/reinstall-vmlinuz /cygdrive/$c/reinstall-initrd\n    fi\n\n    # 使用外部 grub 时，删除外部 grub.cfg\n    if ! is_use_local_grub_extlinux; then\n        rm -f \"$target_cfg\"\n    fi\n\n    if is_in_windows; then\n        if is_efi; then\n            # efi\n            remove_exist_reinstall_efi_dir\n\n            bcdedit /set '{fwbootmgr}' bootsequence '{bootmgr}'\n            bcdedit /enum bootmgr | grep -a -B3 'reinstall' | awk '{print $2}' | grep '{.*}' |\n                xargs -I {} cmd /c bcdedit /delete {}\n        else\n            # bios\n            id='{1c41f649-1637-52f1-aea8-f96bfebeecc8}'\n            if bcdedit /enum all | grep -a \"$id\"; then\n                bcdedit /delete \"$id\"\n            fi\n        fi\n    else\n        if is_efi; then\n            # efi\n\n            # 题外话\n            # 1. 如果用本机的 grub，则 custom.cfg 可能在 efi 分区，也可能在 /boot 分区\n            # 2. 有可能没有 /boot 文件夹\n            #    如果 nixos 的 efi 挂载到 /efi，则不会生成 /boot 文件夹\n            # 3. find 不存在的路径会报错\n            remove_exist_reinstall_efi_dir\n\n            install_pkg efibootmgr\n            efibootmgr | grep -q 'BootNext:' && efibootmgr --quiet --delete-bootnext\n            efibootmgr | grep_efi_entry | grep 'reinstall' | grep_efi_index |\n                xargs -I {} efibootmgr --quiet --bootnum {} --delete-bootnum\n        else\n            # bios\n\n            # 删除 reinstall 条目\n            if [ -f \"$target_cfg\" ]; then\n                sed -i \"/^$BOOT_ENTEY_START_MARK/,/^$BOOT_ENTEY_END_MARK/d\" \"$target_cfg\"\n            fi\n\n            # 清除 next entry\n            if is_use_local_grub; then\n                $grub-editenv - unset next_entry\n            elif is_use_local_extlinux; then\n                extlinux --clear-once \"$(dirname \"$target_cfg\")\"\n            fi\n\n            # 重新创建 grub.cfg / extlinux.conf\n            recreate_grub_or_extlinux_cfg\n        fi\n    fi\n}\n\nreset_and_exit() {\n    from_ctrl_c=${1:-false}\n\n    # info\n    if $from_ctrl_c; then\n        info \"Caught Ctrl+C, reseting...\"\n    fi\n\n    # 清除\n    remove_exist_reinstall\n    rm -rf \"$tmp\"\n    echo \"reset done.\"\n\n    # 退出\n    if $from_ctrl_c; then\n        exit 1\n    else\n        exit 0\n    fi\n}\n\n# 脚本入口\n\n# windows 环境下的额外初始化\nif is_in_windows; then\n    # win系统盘\n    c=$(echo $SYSTEMDRIVE | cut -c1)\n\n    # 64位系统 + 32位cmd/cygwin，需要添加 PATH，否则找不到64位系统程序，例如bcdedit\n    sysnative=$(cygpath -u $WINDIR\\\\Sysnative)\n    if [ -d $sysnative ]; then\n        PATH=$PATH:$sysnative\n    fi\n\n    # 更改 windows 命令输出语言为英文\n    # chcp 会清屏\n    mode.com con cp select=437 >/dev/null\n\n    # 为 windows 程序输出删除 cr\n    for exe in $WINDOWS_EXES; do\n        # 如果我们覆写了 wmic()，则先将 wmic() 重命名为 _wmic()\n        if get_function $exe >/dev/null 2>&1; then\n            eval \"_$(get_function $exe)\"\n        fi\n        # 使用以下方法重新生成 wmic()\n        # 调用链：wmic() -> run_with_del_cr(wmic) -> _wmic() -> command wmic\n        eval \"$exe(){ $(get_function_content run_with_del_cr_template | sed \"s/\\$exe/$exe/g\") }\"\n    done\nfi\n\n# 检查 root\nif is_in_windows; then\n    # 64位系统 + 32位cmd/cygwin，运行 openfiles 报错：目标系统必须运行 32 位的操作系统\n    if ! fltmc >/dev/null 2>&1; then\n        error_and_exit \"Please run as administrator.\"\n    fi\nelse\n    if [ \"$EUID\" -ne 0 ]; then\n        error_and_exit \"Please run as root.\"\n    fi\nfi\n\n# 不支持 Live OS 下运行\nif mount | grep -q 'tmpfs on / type tmpfs'; then\n    error_and_exit \"Can't run this script in Live OS.\"\nfi\n\n# 不支持容器虚拟化\nif is_in_container; then\n    error_and_exit \"Not Supported OS in Container.\\nPlease use https://github.com/LloydAsp/OsMutation\"\nfi\n\n# 不支持安全启动\nif is_secure_boot_enabled; then\n    error_and_exit \"Please disable secure boot first.\"\nfi\n\n# 整理参数\nlong_opts=\nfor o in ci installer debug minimal allow-ping force-cn help \\\n    add-driver: \\\n    hold: sleep: \\\n    iso: \\\n    image-name: \\\n    boot-wim: \\\n    img: \\\n    cloud-data: \\\n    lang: \\\n    passwd: password: \\\n    ssh-port: \\\n    ssh-key: public-key: \\\n    rdp-port: \\\n    web-port: http-port: \\\n    allow-ping: \\\n    commit: \\\n    frpc-conf: frpc-config: \\\n    target-disk: \\\n    force-boot-mode: \\\n    force-old-windows-setup:; do\n    [ -n \"$long_opts\" ] && long_opts+=,\n    long_opts+=$o\ndone\n\n# 使用 getopt 解析参数\nif ! ORIGINAL_OPTS=$(getopt -n $0 -o \"h,x\" --long \"$long_opts\" -- \"$@\"); then\n    exit 1\nfi\n\n# 第一遍扫描，验证要安装的系统和版本\neval set -- \"$ORIGINAL_OPTS\"\nwhile true; do\n    case \"$1\" in\n    -x | --debug)\n        set -x\n        shift\n        ;;\n    --)\n        shift\n        verify_os_name \"$@\"\n        break\n        ;;\n    *)\n        shift\n        ;;\n    esac\ndone\n\n# 初始化重要变量\n# wmic 随时会用到\n# wmic 需要下载 wmic.ps1，需要从 confhome 得到，要先将 confhome 改成国内\n# 而 wmic.ps1 又放在 $tmp 目录，因此要先创建临时目录\n# 处理 --frpc-config 时会下载文件，因此在处理参数前就创建临时目录\nmkdir_clear \"$tmp\"\ninit_basearch\ninit_confhome\ninit_bootloader_facts\n\nif [ \"$distro\" = reset ]; then\n    reset_and_exit\nfi\n\n# 安装必备组件\ninstall_pkg curl grep\n\n# 第二遍扫描，处理参数\neval set -- \"$ORIGINAL_OPTS\"\n# shellcheck disable=SC2034\nwhile true; do\n    case \"$1\" in\n    -x | --debug)\n        # 第一遍扫描已处理\n        shift\n        ;;\n    -h | --help)\n        usage_and_exit\n        ;;\n    --commit)\n        commit=$2\n        shift 2\n        ;;\n    --ci)\n        cloud_image=1\n        unset installer\n        shift\n        ;;\n    --installer)\n        installer=1\n        unset cloud_image\n        shift\n        ;;\n    --minimal)\n        minimal=1\n        shift\n        ;;\n    --allow-ping)\n        allow_ping=1\n        shift\n        ;;\n    --force-cn)\n        # 仅为了方便测试\n        force_cn=1\n        shift\n        ;;\n    --hold | --sleep)\n        if ! { [ \"$2\" = 0 ] || [ \"$2\" = 1 ] || [ \"$2\" = 2 ]; }; then\n            error_and_exit \"Invalid $1 value: $2\"\n        fi\n        hold=$2\n        shift 2\n        ;;\n    --frpc-conf | --frpc-config)\n        [ -n \"$2\" ] || error_and_exit \"Need value for $1\"\n\n        case \"$(to_lower <<<\"$2\")\" in\n        http://* | https://*)\n            frpc_config_url=$2\n            frpc_config=$tmp/frpc.conf\n            # 用 file 识别文件类型？\n            if ! curl -L \"$frpc_config_url\" -o \"$frpc_config\"; then\n                error_and_exit \"Can't get frpc config from $frpc_config_url\"\n            fi\n            ;;\n        *)\n            # windows 路径转换\n            if ! { frpc_config=$(get_unix_path \"$2\") && [ -f \"$frpc_config\" ]; }; then\n                error_and_exit \"File not exists: $2\"\n            fi\n            ;;\n        esac\n\n        # 转为绝对路径\n        frpc_config=$(readlink -f \"$frpc_config\")\n\n        shift 2\n        ;;\n    --force-boot-mode)\n        if ! { [ \"$2\" = bios ] || [ \"$2\" = efi ]; }; then\n            error_and_exit \"Invalid $1 value: $2\"\n        fi\n        force_boot_mode=$2\n        shift 2\n        ;;\n    --passwd | --password)\n        [ -n \"$2\" ] || error_and_exit \"Need value for $1\"\n        password=$2\n        shift 2\n        ;;\n    --ssh-key | --public-key)\n        ssh_key_error_and_exit() {\n            error \"$1\"\n            cat <<EOF\nAvailable options:\n  --ssh-key \"ssh-rsa ...\"\n  --ssh-key \"ssh-ed25519 ...\"\n  --ssh-key \"ecdsa-sha2-nistp256/384/521 ...\"\n  --ssh-key github:your_username\n  --ssh-key gitlab:your_username\n  --ssh-key http://path/to/public_key\n  --ssh-key https://path/to/public_key\n  --ssh-key /path/to/public_key\n  --ssh-key C:\\path\\to\\public_key\nEOF\n            exit 1\n        }\n\n        # https://manpages.debian.org/testing/openssh-server/authorized_keys.5.en.html#AUTHORIZED_KEYS_FILE_FORMAT\n        is_valid_ssh_key() {\n            grep -qE '^(ecdsa-sha2-nistp(256|384|521)|ssh-(ed25519|rsa)) ' <<<\"$1\"\n        }\n\n        [ -n \"$2\" ] || ssh_key_error_and_exit \"Need value for $1\"\n\n        case \"$(to_lower <<<\"$2\")\" in\n        github:* | gitlab:* | http://* | https://*)\n            if [[ \"$(to_lower <<<\"$2\")\" = http* ]]; then\n                key_url=$2\n            else\n                IFS=: read -r site user <<<\"$2\"\n                [ -n \"$user\" ] || ssh_key_error_and_exit \"Need a username for $site\"\n                key_url=\"https://$site.com/$user.keys\"\n            fi\n            if ! ssh_key=$(curl -L \"$key_url\"); then\n                error_and_exit \"Can't get ssh key from $key_url\"\n            fi\n            ;;\n        *)\n            # 检测值是否为 ssh key\n            if is_valid_ssh_key \"$2\"; then\n                ssh_key=$2\n            else\n                # 视为路径\n                # windows 路径转换\n                if ! { ssh_key_file=$(get_unix_path \"$2\") && [ -f \"$ssh_key_file\" ]; }; then\n                    ssh_key_error_and_exit \"SSH Key/File/Url \\\"$2\\\" is invalid.\"\n                fi\n                ssh_key=$(<\"$ssh_key_file\")\n            fi\n            ;;\n        esac\n\n        # 检查 key 格式\n        if ! is_valid_ssh_key \"$ssh_key\"; then\n            ssh_key_error_and_exit \"SSH Key/File/Url \\\"$2\\\" is invalid.\"\n        fi\n\n        # 保存 key\n        # 不用处理注释，可以支持写入 authorized_keys\n        # 安装 nixos 时再处理注释/空行，转成数组，再添加到 nix 配置文件中\n        if [ -n \"$ssh_keys\" ]; then\n            ssh_keys+=$'\\n'\n        fi\n        ssh_keys+=$ssh_key\n\n        shift 2\n        ;;\n    --ssh-port)\n        is_port_valid $2 || error_and_exit \"Invalid $1 value: $2\"\n        ssh_port=$2\n        shift 2\n        ;;\n    --rdp-port)\n        is_port_valid $2 || error_and_exit \"Invalid $1 value: $2\"\n        rdp_port=$2\n        shift 2\n        ;;\n    --web-port | --http-port)\n        is_port_valid $2 || error_and_exit \"Invalid $1 value: $2\"\n        web_port=$2\n        shift 2\n        ;;\n    --add-driver)\n        [ -n \"$2\" ] || error_and_exit \"Need value for $1\"\n\n        # windows 路径转换\n        inf_or_dir=$(get_unix_path \"$2\")\n\n        # alpine busybox 不支持 readlink -m\n        # readlink -m /asfsafasfsaf/fasf\n        # 因此需要先判断路径是否存在\n\n        if ! [ -d \"$inf_or_dir\" ] &&\n            ! { [ -f \"$inf_or_dir\" ] && [[ \"$inf_or_dir\" =~ \\.[iI][nN][fF]$ ]]; }; then\n            error_and_exit \"Not a inf or dir: $2\"\n        fi\n\n        # 转为绝对路径\n        inf_or_dir=$(readlink -f \"$inf_or_dir\")\n\n        info \"finding inf in $inf_or_dir\"\n        # find /tmp -type f -iname '*.inf' 只要 /tmp 存在就会返回 0\n        if infs=$(find \"$inf_or_dir\" -type f -iname '*.inf' | grep .); then\n            while IFS= read -r inf; do\n                # 防止重复添加\n                if ! grep -Fqx \"$inf\" <<<\"$custom_infs\"; then\n                    echo \"inf found: $inf\"\n                    # 一行一个 inf\n                    if [ -n \"$custom_infs\" ]; then\n                        custom_infs+=$'\\n'\n                    fi\n                    custom_infs+=$inf\n                fi\n            done <<<\"$infs\"\n        else\n            error_and_exit \"Can't find inf files in $2\"\n        fi\n\n        shift 2\n        ;;\n    --force-old-windows-setup)\n        force_old_windows_setup=$2\n        shift 2\n        ;;\n    --target-disk)\n        xda=${2##*/dev/}\n        if ! [ -b \"/dev/$xda\" ]; then\n            error_and_exit \"Can't not find Disk $2.\"\n        fi\n        shift 2\n        ;;\n    --img)\n        img=$2\n        shift 2\n        ;;\n    --cloud-data)\n        cloud_data=$2\n        shift 2\n        ;;\n    --iso)\n        iso=$2\n        shift 2\n        ;;\n    --boot-wim)\n        boot_wim=$2\n        shift 2\n        ;;\n    --image-name)\n        image_name=$(echo \"$2\" | to_lower)\n        shift 2\n        ;;\n    --lang)\n        lang=$(echo \"$2\" | to_lower)\n        shift 2\n        ;;\n    --)\n        shift\n        break\n        ;;\n    *)\n        echo \"Unexpected option: $1.\"\n        usage_and_exit\n        ;;\n    esac\ndone\n\n# 检查必须的参数\nverify_os_args\n\n# 密码\nif ! is_netboot_xyz && [ -z \"$ssh_keys\" ] && [ -z \"$password\" ]; then\n    if is_use_dd; then\n        show_dd_password_tips\n    fi\n    prompt_password\nfi\n\n# 强制忽略/强制添加 --ci 参数\n# debian 不强制忽略 ci 留作测试\ncase \"$distro\" in\ndd | windows | netboot.xyz | kali | alpine | arch | gentoo | aosc | nixos | fnos)\n    if is_use_cloud_image; then\n        echo \"ignored --ci\"\n        unset cloud_image\n    fi\n    ;;\noracle | opensuse | anolis | opencloudos | openeuler)\n    cloud_image=1\n    ;;\nredhat | centos | almalinux | rocky | fedora | ubuntu)\n    if is_force_use_installer; then\n        unset cloud_image\n    else\n        cloud_image=1\n    fi\n    ;;\nesac\n\n# 检查内存\n# 会用到 wmic，因此要在设置国内 confhome 后使用\ncheck_ram\n\n# 以下目标系统不需要两步安装\n# alpine\n# debian\n# el7 x86_64 >=1g\n# el7 aarch64 >=1.5g\n# el8/9/fedora 任何架构 >=2g\nif is_netboot_xyz ||\n    { ! is_use_cloud_image && {\n        [ \"$distro\" = \"alpine\" ] || is_distro_like_debian ||\n            { is_distro_like_redhat && [ $releasever -eq 7 ] && [ $ram_size -ge 1024 ] && [ $basearch = \"x86_64\" ]; } ||\n            { is_distro_like_redhat && [ $releasever -eq 7 ] && [ $ram_size -ge 1536 ] && [ $basearch = \"aarch64\" ]; } ||\n            { is_distro_like_redhat && [ $releasever -ge 8 ] && [ $ram_size -ge 2048 ]; }\n    }; }; then\n    setos nextos $distro $releasever\nelse\n    # alpine 作为中间系统时，使用最新版\n    alpine_ver_for_trans=$(get_latest_distro_releasever alpine)\n    setos finalos $distro $releasever\n    setos nextos alpine $alpine_ver_for_trans\nfi\n\n# 有的机器开启了 kexec，例如腾讯云轻量 debian，要禁用\nif [ -f /etc/default/kexec ]; then\n    sed -i 's/LOAD_KEXEC=true/LOAD_KEXEC=false/' /etc/default/kexec\nfi\n\n# 一切就绪，正式下载内核和添加引导项\n# 先删除之前的启动项，再设置 trap\n# 暂时不用 trap，因为 bat 下无效\nremove_exist_reinstall\n# trap 'reset_and_exit true' SIGINT\n\n# 下载 netboot.xyz / 内核\n# shellcheck disable=SC2154\nif is_netboot_xyz; then\n    if is_efi; then\n        if is_in_windows; then\n            add_efi_entry_in_windows $nextos_efi\n        else\n            add_efi_entry_in_linux $nextos_efi\n        fi\n    else\n        curl -Lo /reinstall-vmlinuz $nextos_vmlinuz\n    fi\nelse\n    # 下载 nextos 内核\n    info download vmlnuz and initrd\n    curl -Lo /reinstall-vmlinuz $nextos_vmlinuz\n    curl -Lo /reinstall-initrd $nextos_initrd\n    if is_use_firmware; then\n        curl -Lo /reinstall-firmware $nextos_firmware\n    fi\nfi\n\n# 修改 alpine debian kali initrd\nif [ \"$nextos_distro\" = alpine ] || is_distro_like_debian \"$nextos_distro\"; then\n    mod_initrd\nfi\n\n# 将内核/netboot.xyz.lkrn 放到正确的位置\nif false && is_need_boot_vmlinuz; then\n    if is_in_windows; then\n        cp -f /reinstall-vmlinuz /cygdrive/$c/\n        is_have_initrd && cp -f /reinstall-initrd /cygdrive/$c/\n    else\n        if is_os_in_btrfs && is_os_in_subvol; then\n            cp_to_btrfs_root /reinstall-vmlinuz\n            is_have_initrd && cp_to_btrfs_root /reinstall-initrd\n        fi\n    fi\nfi\n\n# 需要使用 vmlinuz/initrd 引导的情况\nif is_need_boot_vmlinuz; then\n    # win 使用外部 grub\n    if is_in_windows; then\n        install_grub_win\n    else\n        # linux efi 使用外部 grub，因为\n        # 1. 原系统 grub 可能没有去除 aarch64 内核 magic number 校验\n        # 2. 原系统可能不是用 grub\n        if is_efi; then\n            install_grub_linux_efi\n        fi\n    fi\n\n    # 找到 /reinstall-vmlinuz /reinstall-initrd 的绝对路径\n    if is_in_windows; then\n        # dir=/cygwin/\n        dir=$(cygpath -m / | cut -d: -f2-)/\n    else\n        # extlinux + 单独的 boot 分区\n        # 把内核文件放在 extlinux.conf 所在的目录\n        if is_use_local_extlinux && is_boot_in_separate_partition; then\n            dir=\n        else\n            # 获取当前系统根目录在 btrfs 中的绝对路径\n            if is_os_in_btrfs; then\n                # btrfs subvolume show /\n                # 输出可能是 / 或 root 或 @/.snapshots/1/snapshot\n                dir=$(btrfs subvolume show / | head -1)\n                if ! [ \"$dir\" = / ]; then\n                    dir=\"/$dir/\"\n                fi\n            else\n                dir=/\n            fi\n        fi\n    fi\n\n    vmlinuz=${dir}reinstall-vmlinuz\n    initrd=${dir}reinstall-initrd\n    firmware=${dir}reinstall-firmware\n\n    # 设置 linux initrd 命令\n    if is_use_local_extlinux; then\n        linux_cmd=LINUX\n        initrd_cmd=INITRD\n    else\n        if is_netboot_xyz; then\n            linux_cmd=linux16\n            initrd_cmd=initrd16\n        else\n            linux_cmd=linux\n            initrd_cmd=initrd\n        fi\n    fi\n\n    # 设置 cmdlind initrds\n    if ! is_netboot_xyz; then\n        find_main_disk\n        build_cmdline\n\n        initrds=\"$initrd\"\n        if is_use_firmware; then\n            initrds+=\" $firmware\"\n        fi\n    fi\n\n    if is_use_local_extlinux; then\n        info extlinux\n        echo \"$target_cfg\"\n        extlinux_dir=\"$(dirname \"$target_cfg\")\"\n\n        # 不起作用\n        # 好像跟 extlinux --once 有冲突\n        sed -i \"/^MENU HIDDEN/d\" \"$target_cfg\"\n        sed -i \"/^TIMEOUT /d\" \"$target_cfg\"\n\n        del_empty_lines <<EOF | tee -a \"$target_cfg\"\n$BOOT_ENTEY_START_MARK\nTIMEOUT 5\nLABEL reinstall\n  MENU LABEL $(get_entry_name)\n  $linux_cmd $vmlinuz\n  $([ -n \"$initrds\" ] && echo \"$initrd_cmd $initrds\")\n  $([ -n \"$cmdline\" ] && echo \"APPEND $cmdline\")\n$BOOT_ENTEY_END_MARK\nEOF\n        # 设置重启引导项\n        extlinux --once=reinstall $extlinux_dir\n\n        # 复制文件到 extlinux 工作目录\n        if is_boot_in_separate_partition; then\n            info \"copying files to $extlinux_dir\"\n            is_have_initrd && cp -f /reinstall-initrd $extlinux_dir\n            is_use_firmware && cp -f /reinstall-firmware $extlinux_dir\n            # 放最后，防止前两条返回非 0 而报错\n            cp -f /reinstall-vmlinuz $extlinux_dir\n        fi\n    else\n        # cloudcone 从光驱的 grub 启动，再加载硬盘的 grub.cfg\n        # menuentry \"Grub 2\" --id grub2 {\n        #         set root=(hd0,msdos1)\n        #         configfile /boot/grub2/grub.cfg\n        # }\n\n        # 加载后 $prefix 依然是光驱的 (hd96)/boot/grub\n        # 导致找不到 $prefix 目录的 grubenv，因此读取不到 next_entry\n        # 以下方法为 cloudcone 重新加载 grubenv\n\n        # 需查找 2*2 个文件夹\n        # 分区：系统 / boot\n        # 文件夹：grub / grub2\n        # shellcheck disable=SC2121,SC2154\n        # cloudcone debian 能用但 ubuntu 模板用不了\n        # ubuntu 模板甚至没显示 reinstall menuentry\n        load_grubenv_if_not_loaded() {\n            if ! [ -s $prefix/grubenv ]; then\n                for dir in /boot/grub /boot/grub2 /grub /grub2; do\n                    set grubenv=\"($root)$dir/grubenv\"\n                    if [ -s $grubenv ]; then\n                        load_env --file $grubenv\n                        if [ \"${next_entry}\" ]; then\n                            set default=\"${next_entry}\"\n                            set next_entry=\n                            save_env --file $grubenv next_entry\n                        else\n                            set default=\"0\"\n                        fi\n                        return\n                    fi\n                done\n            fi\n        }\n\n        # 生成 grub 配置\n        # 实测 centos 7 lvm 要手动加载 lvm 模块\n        info grub\n        echo $target_cfg\n\n        echo '### BEGIN reinstall.sh ###' >$target_cfg\n\n        get_function_content load_grubenv_if_not_loaded >>$target_cfg\n\n        # 原系统为 openeuler 云镜像，需要添加 --unrestricted，否则要输入密码\n        del_empty_lines <<EOF | del_comment_lines | tee -a $target_cfg\nset timeout_style=menu\nset timeout=5\nmenuentry \"$(get_entry_name)\" --unrestricted {\n    $(! is_in_windows && echo 'insmod lvm')\n    $(is_os_in_btrfs && echo 'set btrfs_relative_path=n')\n    # fedora efi 没有 load_video\n    insmod all_video\n    # set gfxmode=800x600\n    # set gfxpayload=keep\n    # terminal_output gfxterm 在 vultr 上会花屏\n    # terminal_output console\n    search --no-floppy --file --set=root $vmlinuz\n    $linux_cmd $vmlinuz $cmdline\n    $([ -n \"$initrds\" ] && echo \"$initrd_cmd $initrds\")\n}\nEOF\n        echo '### END reinstall.sh ###' >>$target_cfg\n\n        # 设置重启引导项\n        if is_use_local_grub; then\n            $grub-reboot \"$(get_entry_name)\"\n        fi\n    fi\nfi\n\ninfo 'info'\necho \"$distro $releasever\"\n\ncase \"$distro\" in\nwindows) username=administrator ;;\nnetboot.xyz) username= ;;\ndd | *) username=root ;;\nesac\n\nif [ -n \"$username\" ]; then\n    echo \"Username: $username\"\n    if [ -n \"$ssh_keys\" ]; then\n        echo \"Public Key: $ssh_keys\"\n    else\n        echo \"Password: $password\"\n    fi\nfi\n\nif is_netboot_xyz; then\n    echo 'Reboot to start netboot.xyz.'\nelif is_alpine_live; then\n    echo 'Reboot to start Alpine Live OS.'\nelif is_use_dd; then\n    if [ -n \"$cloud_data\" ]; then\n        echo \"Cloud Data: $cloud_data\"\n        echo \"Cloud Data Files: $cloud_data_files\"\n    fi\n    show_dd_password_tips\n    echo 'Reboot to start DD.'\nelif [ \"$distro\" = fnos ]; then\n    echo \"Special note for FNOS:\"\n    echo \"Reboot to start the installation.\"\n    echo \"SSH login is disabled when installation completed.\"\n    echo \"You need to config the account and password on http://SERVER_IP:5666 as soon as possible.\"\n    echo\n    echo \"飞牛 OS 注意事项：\"\n    echo \"重启后开始安装。\"\n    echo \"安装完成后不支持 SSH 登录。\"\n    echo \"你需要尽快在 http://SERVER_IP:5666 配置账号密码。\"\nelse\n    echo \"Reboot to start the installation.\"\nfi\n\nif is_in_windows; then\n    echo 'You can run this command to reboot:'\n    echo 'shutdown /r /t 0'\nfi\n\necho\necho \"If you want to revert all changes made by this script, run \\\"$reinstall_____ reset\\\"\"\necho\n"
  },
  {
    "path": "resize.sh",
    "content": "#!/bin/bash\nPATH=\"/usr/sbin:/usr/bin\"\n\nupdate_part() {\n    partx -u \"$1\"\n    udevadm trigger\n    udevadm settle\n}\n\n# el 自带 fdisk parted (el7的part不支持在线扩容)\n# ubuntu 自带 fdisk growpart\n\n# 删除分区用\n# el/ubuntu fdisk\n\n# 扩容分区用\n# el7 grownparted 额外安装\n# el8/9/fedora parted\n# ubuntu grownpart\n\n# 找出主硬盘\nroot_drive=$(mount | awk '$3==\"/\" {print $1}')\nxda=$(lsblk -r --inverse \"$root_drive\" | grep -w disk | awk '{print $1}')\n\n# 删除 installer 分区\ninstaller_num=$(readlink -f /dev/disk/by-label/installer | grep -o '[0-9]*$')\nif [ -n \"$installer_num\" ]; then\n    # 要添加 LC_NUMERIC 或者将%转义成\\%才能在cron里正确运行\n    # locale -a 不一定有\"en_US.UTF-8\"，但肯定有\"C.UTF-8\"\n    LC_NUMERIC=\"C.UTF-8\"\n    printf \"d\\n%s\\nw\" \"$installer_num\" | fdisk \"/dev/$xda\"\n    update_part \"/dev/$xda\"\nfi\n\n# 找出现在的最后一个分区，也就是系统分区\n# el7 的 lsblk 没有 --sort，所以用其他方法\n# shellcheck disable=2012\npart_num=$(ls -1v \"/dev/$xda\"* | tail -1 | grep -o '[0-9]*$')\npart_fstype=$(lsblk -no FSTYPE \"/dev/$xda\"*\"$part_num\")\n\n# 扩容分区\n# ubuntu 和 el7 用 growpart，其他用 parted\n# el7 不能用parted在线扩容，而fdisk扩容会改变 PARTUUID，所以用 growpart\nif grep -E -i 'centos:7|ubuntu' /etc/os-release; then\n    growpart \"/dev/$xda\" \"$part_num\"\nelse\n    printf 'yes\\n100%%' | parted \"/dev/$xda\" resizepart \"$part_num\" ---pretend-input-tty\nfi\nupdate_part \"/dev/$xda\"\n\n# 扩容最后一个分区的文件系统\ncase $part_fstype in\nxfs) xfs_growfs / ;;\next*) resize2fs \"/dev/$xda\"*\"$part_num\" ;;\nbtrfs) btrfs filesystem resize max / ;;\nesac\nupdate_part \"/dev/$xda\"\n\n# 删除脚本自身\nrm -f /resize.sh /etc/cron.d/resize\n"
  },
  {
    "path": "trans.sh",
    "content": "#!/bin/ash\n# shellcheck shell=dash\n# shellcheck disable=SC2086,SC3047,SC3036,SC3010,SC3001,SC3060\n# alpine 默认使用 busybox ash\n# 注意 bash 和 ash 以下语句结果不同\n# [[ a = '*a' ]] && echo 1\n\n# 出错后停止运行，将进入到登录界面，防止失联\nset -eE\n\n# 用于判断 reinstall.sh 和 trans.sh 是否兼容\n# shellcheck disable=SC2034\nSCRIPT_VERSION=4BACD833-A585-23BA-6CBB-9AA4E08E0004\n\nTRUE=0\nFALSE=1\nEFI_UUID=C12A7328-F81F-11D2-BA4B-00A0C93EC93B\n\nerror() {\n    color='\\e[31m'\n    plain='\\e[0m'\n    echo -e \"${color}***** ERROR *****${plain}\" >&2\n    echo -e \"${color}$*${plain}\" >&2\n}\n\ninfo() {\n    color='\\e[32m'\n    plain='\\e[0m'\n    local msg\n\n    if [ \"$1\" = false ]; then\n        shift\n        msg=$*\n    else\n        msg=$(echo \"$*\" | to_upper)\n    fi\n\n    echo -e \"${color}***** $msg *****${plain}\" >&2\n}\n\nwarn() {\n    color='\\e[33m'\n    plain='\\e[0m'\n    echo -e \"${color}Warning: $*${plain}\" >&2\n}\n\nerror_and_exit() {\n    error \"$@\"\n    echo \"Run '/trans.sh' to retry.\" >&2\n    echo \"Run '/trans.sh alpine' to install Alpine Linux instead.\" >&2\n    exit 1\n}\n\ntrap_err() {\n    line_no=$1\n    ret_no=$2\n\n    error_and_exit \"$(\n        echo \"Line $line_no return $ret_no\"\n        if [ -f \"/trans.sh\" ]; then\n            sed -n \"$line_no\"p /trans.sh\n        fi\n    )\"\n}\n\nis_run_from_locald() {\n    [[ \"$0\" = \"/etc/local.d/*\" ]]\n}\n\nadd_community_repo() {\n    # 先检查原来的repo是不是egde\n    if grep -q '^http.*/edge/main$' /etc/apk/repositories; then\n        alpine_ver=edge\n    else\n        alpine_ver=v$(cut -d. -f1,2 </etc/alpine-release)\n    fi\n\n    if ! grep -q \"^http.*/$alpine_ver/community$\" /etc/apk/repositories; then\n        alpine_mirror=$(grep '^http.*/main$' /etc/apk/repositories | sed 's,/[^/]*/main$,,' | head -1)\n        echo $alpine_mirror/$alpine_ver/community >>/etc/apk/repositories\n    fi\n}\n\n# 有时网络问题下载失败，导致脚本中断\n# 因此需要重试\napk() {\n    retry 5 command apk \"$@\" >&2\n}\n\nshow_url_in_args() {\n    while [ $# -gt 0 ]; do\n        case \"$1\" in\n        [Hh][Tt][Tt][Pp][Ss]://* | [Hh][Tt][Tt][Pp]://* | [Mm][Aa][Gg][Nn][Ee][Tt]:*) echo \"$1\" ;;\n        esac\n        shift\n    done\n}\n\n# 在没有设置 set +o pipefail 的情况下，限制下载大小：\n# retry 5 command wget | head -c 1048576 会触发 retry，下载 5 次\n# command wget \"$@\" --tries=5 | head -c 1048576 不会触发 wget 自带的 retry，只下载 1 次\nwget() {\n    show_url_in_args \"$@\" >&2\n    if command wget 2>&1 | grep -q BusyBox; then\n        # busybox wget 没有重试功能\n        # 好像默认永不超时\n        retry 5 command wget \"$@\" -T 10\n    else\n        # 原版 wget 自带重试功能\n        command wget --tries=5 --progress=bar:force \"$@\"\n    fi\n}\n\nis_have_cmd() {\n    # command -v 包括脚本里面的方法\n    is_have_cmd_on_disk / \"$1\"\n}\n\nis_have_cmd_on_disk() {\n    local os_dir=$1\n    local cmd=$2\n\n    for bin_dir in /bin /sbin /usr/bin /usr/sbin; do\n        if [ -f \"$os_dir$bin_dir/$cmd\" ]; then\n            return\n        fi\n    done\n    return 1\n}\n\nis_num() {\n    echo \"$1\" | grep -Exq '[0-9]*\\.?[0-9]*'\n}\n\nretry() {\n    local max_try=$1\n    shift\n\n    if is_num \"$1\"; then\n        local interval=$1\n        shift\n    else\n        local interval=5\n    fi\n\n    for i in $(seq $max_try); do\n        if \"$@\"; then\n            return\n        else\n            ret=$?\n            if [ $i -ge $max_try ]; then\n                return $ret\n            fi\n            sleep $interval\n        fi\n    done\n}\n\nget_url_type() {\n    if [[ \"$1\" = magnet:* ]]; then\n        echo bt\n    else\n        echo http\n    fi\n}\n\nis_magnet_link() {\n    [[ \"$1\" = magnet:* ]]\n}\n\ndownload() {\n    url=$1\n    path=$2\n\n    # 有ipv4地址无ipv4网关的情况下，aria2可能会用ipv4下载，而不是ipv6\n    # axel 在 lightsail 上会占用大量cpu\n    # https://download.opensuse.org/distribution/leap/15.5/appliances/openSUSE-Leap-15.5-Minimal-VM.x86_64-kvm-and-xen.qcow2\n    # https://aria2.github.io/manual/en/html/aria2c.html#cmdoption-o\n\n    # 阿里云源限速，而且检测 user-agent 禁止 axel/aria2 下载\n    # aria2 默认 --max-tries 5\n\n    # 默认 --max-tries=5，但以下情况服务器出错，aria2不会重试，而是直接返回错误\n    # 因此添加 for 循环\n    #     [ERROR] CUID#7 - Download aborted. URI=https://aka.ms/manawindowsdrivers\n    # Exception: [AbstractCommand.cc:351] errorCode=1 URI=https://aka.ms/manawindowsdrivers\n    #   -> [SocketCore.cc:1019] errorCode=1 SSL/TLS handshake failure:  `not signed by known authorities or invalid'\n\n    # 用 if 的话，报错不会中断脚本\n    # if aria2c xxx; then\n    #     return\n    # fi\n\n    # --user-agent=Wget/1.21.1 \\\n    # --retry-wait 5\n\n    # 检测大小时已经下载了种子\n    if [ \"$(get_url_type \"$url\")\" = bt ]; then\n        torrent=\"$(get_torrent_path_by_magnet $url)\"\n        if ! [ -f \"$torrent\" ]; then\n            download_torrent_by_magnet \"$url\" \"$torrent\"\n        fi\n        url=$torrent\n    fi\n\n    # intel 禁止了 aria2 下载驱动\n    # intel 禁止了 wget 下载网页内容\n    # 腾讯云 virtio 驱动也禁止了 aria2 下载\n\n    # -o 设置 http 下载文件名\n    # -O 设置 bt 首个文件的文件名\n    aria2c \"$url\" \\\n        -d \"$(dirname \"$path\")\" \\\n        -o \"$(basename \"$path\")\" \\\n        -O \"1=$(basename \"$path\")\" \\\n        -U curl/7.54.1\n\n    # opensuse 官方镜像支持 metalink\n    # aira2 无法重命名用 metalink 下载的文件\n    # 需用以下方法重命名\n    if head -c 1024 \"$path\" | grep -Fq 'urn:ietf:params:xml:ns:metalink'; then\n        real_file=$(tr -d '\\n' <\"$path\" | sed -E 's|.*<file[[:space:]]+name=\"([^\"]*)\".*|\\1|')\n        mv \"$(dirname \"$path\")/$real_file\" \"$path\"\n    fi\n}\n\nupdate_part() {\n    sleep 1\n    sync\n\n    # partprobe\n    # 有分区挂载中会报 Resource busy 错误\n    if is_have_cmd partprobe; then\n        partprobe /dev/$xda 2>/dev/null || true\n    fi\n\n    # partx\n    # https://access.redhat.com/solutions/199573\n    if is_have_cmd partx; then\n        partx -u /dev/$xda\n    fi\n\n    # mdev\n    # mdev 不会删除 /dev/disk/ 的旧分区，因此手动删除\n    # 如果 rm -rf 的时候刚好 mdev 在创建链接，rm -rf 会报错 Directory not empty\n    # 因此要先停止 mdev 服务\n    # 还要删除 /dev/$xda*?\n    ensure_service_stopped mdev\n    # 即使停止了 mdev，有时也会报 Directory not empty，因此添加 retry\n    retry 5 rm -rf /dev/disk/*\n\n    # 没挂载 modloop 时会提示\n    # modprobe: can't change directory to '/lib/modules': No such file or directory\n    # 因此强制不显示上面的提示\n    mdev -sf 2>/dev/null\n    ensure_service_started mdev 2>/dev/null\n    sleep 1\n}\n\nis_efi() {\n    if [ -n \"$force_boot_mode\" ]; then\n        [ \"$force_boot_mode\" = efi ]\n    else\n        [ -d /sys/firmware/efi/ ]\n    fi\n}\n\nis_use_cloud_image() {\n    [ -n \"$cloud_image\" ] && [ \"$cloud_image\" = 1 ]\n}\n\nis_allow_ping() {\n    [ -n \"$allow_ping\" ] && [ \"$allow_ping\" = 1 ]\n}\n\nsetup_nginx() {\n    apk add nginx\n    # shellcheck disable=SC2154\n    wget $confhome/logviewer.html -O /logviewer.html\n    wget $confhome/logviewer-nginx.conf -O /etc/nginx/http.d/default.conf\n\n    if [ -z \"$web_port\" ]; then\n        web_port=80\n    fi\n    sed -i \"s/@WEB_PORT@/$web_port/gi\" /etc/nginx/http.d/default.conf\n\n    # rc-service -q nginx start\n    if pgrep nginx >/dev/null; then\n        nginx -s reload\n    else\n        nginx\n    fi\n}\n\nsetup_websocketd() {\n    apk add websocketd\n    wget $confhome/logviewer.html -O /tmp/index.html\n    apk add coreutils\n\n    if [ -z \"$web_port\" ]; then\n        web_port=80\n    fi\n\n    pkill websocketd || true\n    # websocketd 遇到 \\n 才推送，因此要转换 \\r 为 \\n\n    websocketd --port \"$web_port\" --loglevel=fatal --staticdir=/tmp \\\n        stdbuf -oL -eL sh -c \"tail -fn+0 /reinstall.log | tr '\\r' '\\n' | grep -Fiv -e password -e token\" &\n}\n\nget_approximate_ram_size() {\n    # lsmem 需要 util-linux\n    if false && is_have_cmd lsmem; then\n        ram_size=$(lsmem -b 2>/dev/null | grep 'Total online memory:' | awk '{ print $NF/1024/1024 }')\n    fi\n\n    if [ -z $ram_size ]; then\n        ram_size=$(free -m | awk '{print $2}' | sed -n '2p')\n    fi\n\n    echo \"$ram_size\"\n}\n\nsetup_web_if_enough_ram() {\n    total_ram=$(get_approximate_ram_size)\n    # 512内存才安装\n    if [ \"$total_ram\" -ge 400 ]; then\n        # lighttpd 虽然运行占用内存少，但安装占用空间大\n        # setup_lighttpd\n        # setup_nginx\n        setup_websocketd\n    fi\n}\n\nsetup_lighttpd() {\n    apk add lighttpd\n    ln -sf /reinstall.html /var/www/localhost/htdocs/index.html\n    rc-service -q lighttpd start\n}\n\nget_ttys() {\n    prefix=$1\n    # shellcheck disable=SC2154\n    wget $confhome/ttys.sh -O- | sh -s $prefix\n}\n\nfind_xda() {\n    # 出错后再运行脚本，硬盘可能已经格式化，之前记录的分区表 id 无效\n    # 因此找到 xda 后要保存 xda 到 /configs/xda\n\n    # 先读取之前保存的\n    if xda=$(get_config xda 2>/dev/null) && [ -n \"$xda\" ]; then\n        return\n    fi\n\n    # 防止 $main_disk 为空\n    if [ -z \"$main_disk\" ]; then\n        error_and_exit \"cmdline main_disk is empty.\"\n    fi\n\n    # busybox fdisk/lsblk/blkid 不显示 mbr 分区表 id\n    # 可用以下工具：\n    # fdisk 在 util-linux-misc 里面，占用大\n    # sfdisk 占用小\n    # lsblk\n    # blkid\n\n    tool=sfdisk\n\n    is_have_cmd $tool && need_install_tool=false || need_install_tool=true\n    if $need_install_tool; then\n        apk add $tool\n    fi\n\n    if [ \"$tool\" = sfdisk ]; then\n        # sfdisk\n        for disk in $(get_all_disks); do\n            if sfdisk --disk-id \"/dev/$disk\" | sed 's/0x//' | grep -ix \"$main_disk\"; then\n                xda=$disk\n                break\n            fi\n        done\n    else\n        # lsblk\n        xda=$(lsblk --nodeps -rno NAME,PTUUID | grep -iw \"$main_disk\" | awk '{print $1}')\n    fi\n\n    if [ -n \"$xda\" ]; then\n        set_config xda \"$xda\"\n    else\n        error_and_exit \"Could not find xda: $main_disk\"\n    fi\n\n    if $need_install_tool; then\n        apk del $tool\n    fi\n}\n\nget_all_disks() {\n    # shellcheck disable=SC2010\n    ls /sys/block/ | grep -Ev '^(loop|sr|nbd)'\n}\n\nextract_env_from_cmdline() {\n    # 提取 finalos/extra 到变量\n    for prefix in finalos extra; do\n        while read -r line; do\n            if [ -n \"$line\" ]; then\n                key=$(echo $line | cut -d= -f1)\n                value=$(echo $line | cut -d= -f2-)\n                eval \"$key='$value'\"\n            fi\n        done < <(xargs -n1 </proc/cmdline | grep \"^${prefix}_\" | sed \"s/^${prefix}_//\")\n    done\n}\n\nensure_service_started() {\n    local service=$1\n\n    if ! rc-service -q \"$service\" start; then\n        for i in $(seq 10); do\n            if [ \"$service\" = modloop ]; then\n                # 避免有时 modloop 下载不完整导致报错\n                # * Failed to verify signature of !\n                # mount: mounting /dev/loop0 on /.modloop failed: Invalid argument\n                rm -f /lib/modloop-lts /lib/modloop-virt\n            fi\n            if rc-service -q \"$service\" start; then\n                return\n            fi\n            sleep 5\n        done\n        error_and_exit \"Failed to start $service.\"\n    fi\n}\n\nensure_service_stopped() {\n    local service=$1\n\n    if ! retry 10 5 rc-service -q \"$service\" stop; then\n        error_and_exit \"Failed to stop $service.\"\n    fi\n}\n\nmod_motd() {\n    # 安装后 alpine 后要恢复默认\n    # 自动安装失败后，可能手动安装 alpine，因此无需判断 $distro\n    file=/etc/motd\n    if ! [ -e $file.orig ]; then\n        cp $file $file.orig\n        # shellcheck disable=SC2016\n        echo \"mv \"\\$mnt$file.orig\" \"\\$mnt$file\"\" |\n            insert_into_file \"$(which setup-disk)\" before 'cleanup_chroot_mounts \"\\$mnt\"'\n\n        cat <<EOF >$file\nReinstalling...\nTo view logs run:\ntail -fn+1 /reinstall.log\nEOF\n    fi\n}\n\numount_all() {\n    dirs=\"/mnt /os /iso /wim /installer /nbd /nbd-boot /nbd-efi /nbd-test /root /nix\"\n    regex=$(echo \"$dirs\" | sed 's, ,|,g')\n    if mounts=$(mount | grep -Ew \"on $regex\" | awk '{print $3}' | tac); then\n        for mount in $mounts; do\n            echo \"umount $mount\"\n            umount $mount\n        done\n    fi\n}\n\n# 可能脚本不是首次运行，先清理之前的残留\nclear_previous() {\n    if is_have_cmd vgchange; then\n        umount -R /os /nbd || true\n        vgchange -an\n        apk add device-mapper\n        dmsetup remove_all\n    fi\n    disconnect_qcow\n    # 安装 arch 有 gpg-agent 进程驻留\n    pkill gpg-agent || true\n    rc-service -q --ifexists --ifstarted nix-daemon stop\n    swapoff -a\n    umount_all\n\n    # 以下情况 umount -R /1 会提示 busy\n    # mount /file1 /1\n    # mount /1/file2 /2\n}\n\n# virt-what 自动安装 dmidecode，因此同时缓存\ncache_dmi_and_virt() {\n    if ! [ \"$_dmi_and_virt_cached\" = 1 ]; then\n        apk add virt-what\n\n        # 区分 kvm 和 virtio，原因:\n        # 1. 阿里云 c8y virt-what 不显示 kvm\n        # 2. 不是所有 kvm 都需要 virtio 驱动，例如 aws nitro\n        # 3. virt-what 不会检测 virtio\n        _virt=$(\n            virt-what\n\n            # hyper-v 环境下 modprobe virtio_scsi 也会创建 /sys/bus/virtio/drivers/virtio_scsi\n            # 因此用 devices 判断更准确，有设备时才有 /sys/bus/virtio/drivers/*\n            # 或者加上 lspci 检测?\n\n            # 不要用 ls /sys/bus/virtio/devices/* && echo virtio\n            # 因为有可能返回值不为 0 而中断脚本\n            if ls /sys/bus/virtio/devices/* >/dev/null 2>&1; then\n                echo virtio\n            fi\n        )\n\n        _dmi=$(dmidecode | grep -E '(Manufacturer|Asset Tag|Vendor): ' | awk -F': ' '{print $2}')\n        _dmi_and_virt_cached=1\n        apk del virt-what\n    fi\n}\n\nis_virt() {\n    cache_dmi_and_virt\n    [ -n \"$_virt\" ]\n}\n\nis_virt_contains() {\n    cache_dmi_and_virt\n    echo \"$_virt\" | grep -Eiwq \"$1\"\n}\n\nis_dmi_contains() {\n    # Manufacturer: Alibaba Cloud\n    # Manufacturer: Tencent Cloud\n    # Manufacturer: Huawei Cloud\n    # Asset Tag: OracleCloud.com\n    # Vendor: Amazon EC2\n    # Manufacturer: Amazon EC2\n    # Asset Tag: Amazon EC2\n    cache_dmi_and_virt\n    echo \"$_dmi\" | grep -Eiwq \"$1\"\n}\n\ncache_lspci() {\n    if [ -z \"$_lspci\" ]; then\n        apk add pciutils\n        _lspci=$(lspci)\n        apk del pciutils\n    fi\n}\n\nis_lspci_contains() {\n    cache_lspci\n    echo \"$_lspci\" | grep -Eiwq \"$1\"\n}\n\nget_config() {\n    cat \"/configs/$1\"\n}\n\nset_config() {\n    printf '%s' \"$2\" >\"/configs/$1\"\n}\n\n# ubuntu 安装版、el/ol 安装版不使用该密码\nget_password_linux_sha512() {\n    get_config password-linux-sha512\n}\n\nget_password_windows_administrator_base64() {\n    get_config password-windows-administrator-base64\n}\n\nget_password_plaintext() {\n    get_config password-plaintext\n}\n\nis_password_plaintext() {\n    get_password_plaintext >/dev/null 2>&1\n}\n\nshow_netconf() {\n    grep -r . /dev/netconf/\n}\n\nget_ra_to() {\n    if [ -z \"$_ra\" ]; then\n        apk add ndisc6\n        # 有时会重复收取，所以设置收一份后退出\n        echo \"Gathering network info...\"\n        # shellcheck disable=SC2154\n        _ra=\"$(rdisc6 -1 \"$ethx\")\"\n        apk del ndisc6\n\n        # 显示网络配置\n        info \"Network info:\"\n        echo\n        echo \"$_ra\" | cat -n\n        echo\n        ip addr | cat -n\n        echo\n        show_netconf | cat -n\n        echo\n    fi\n    eval \"$1='$_ra'\"\n}\n\nget_netconf_to() {\n    case \"$1\" in\n    slaac | dhcpv6 | rdnss | other) get_ra_to ra ;;\n    esac\n\n    # shellcheck disable=SC2154\n    # debian initrd 没有 xargs\n    case \"$1\" in\n    slaac) echo \"$ra\" | grep 'Autonomous address conf' | grep -q Yes && res=1 || res=0 ;;\n    dhcpv6) echo \"$ra\" | grep 'Stateful address conf' | grep -q Yes && res=1 || res=0 ;;\n    rdnss) res=$(echo \"$ra\" | grep 'Recursive DNS server' | cut -d: -f2-) ;;\n    other) echo \"$ra\" | grep 'Stateful other conf' | grep -q Yes && res=1 || res=0 ;;\n    *) res=$(cat /dev/netconf/$ethx/$1) ;;\n    esac\n\n    eval \"$1='$res'\"\n}\n\nis_any_ipv4_has_internet() {\n    grep -q 1 /dev/netconf/*/ipv4_has_internet\n}\n\nis_in_china() {\n    grep -q 1 /dev/netconf/*/is_in_china\n}\n\n# 有 dhcpv4 不等于有网关，例如 vultr 纯 ipv6\n# 没有 dhcpv4 不等于是静态ip，可能是没有 ip\nis_dhcpv4() {\n    if ! is_ipv4_has_internet || should_disable_dhcpv4; then\n        return 1\n    fi\n\n    get_netconf_to dhcpv4\n    # shellcheck disable=SC2154\n    [ \"$dhcpv4\" = 1 ]\n}\n\nis_staticv4() {\n    if ! is_ipv4_has_internet; then\n        return 1\n    fi\n\n    if ! is_dhcpv4; then\n        get_netconf_to ipv4_addr\n        get_netconf_to ipv4_gateway\n        if [ -n \"$ipv4_addr\" ] && [ -n \"$ipv4_gateway\" ]; then\n            return 0\n        fi\n    fi\n    return 1\n}\n\nis_staticv6() {\n    if ! is_ipv6_has_internet; then\n        return 1\n    fi\n\n    if ! is_slaac && ! is_dhcpv6; then\n        get_netconf_to ipv6_addr\n        get_netconf_to ipv6_gateway\n        if [ -n \"$ipv6_addr\" ] && [ -n \"$ipv6_gateway\" ]; then\n            return 0\n        fi\n    fi\n    return 1\n}\n\nis_dhcpv6_or_slaac() {\n    get_netconf_to dhcpv6_or_slaac\n    # shellcheck disable=SC2154\n    [ \"$dhcpv6_or_slaac\" = 1 ]\n}\n\nis_ipv4_has_internet() {\n    get_netconf_to ipv4_has_internet\n    # shellcheck disable=SC2154\n    [ \"$ipv4_has_internet\" = 1 ]\n}\n\nis_ipv6_has_internet() {\n    get_netconf_to ipv6_has_internet\n    # shellcheck disable=SC2154\n    [ \"$ipv6_has_internet\" = 1 ]\n}\n\nshould_disable_dhcpv4() {\n    get_netconf_to should_disable_dhcpv4\n    # shellcheck disable=SC2154\n    [ \"$should_disable_dhcpv4\" = 1 ]\n}\n\nshould_disable_accept_ra() {\n    get_netconf_to should_disable_accept_ra\n    # shellcheck disable=SC2154\n    [ \"$should_disable_accept_ra\" = 1 ]\n}\n\nshould_disable_autoconf() {\n    get_netconf_to should_disable_autoconf\n    # shellcheck disable=SC2154\n    [ \"$should_disable_autoconf\" = 1 ]\n}\n\nis_slaac() {\n    # 如果是静态（包括自动获取到 IP 但无法联网而切换成静态）直接返回 1，不考虑 ra\n    # 防止部分机器slaac/dhcpv6获取的ip/网关无法上网\n\n    # 有可能 ra 的 dhcpv6/slaac 是打开的，但实测无法获取到 ipv6 地址\n    # is_dhcpv6_or_slaac 是实测结果，因此如果实测不通过，也返回 1\n\n    # 不要判断 is_staticv6，因为这会导致死循环\n    if ! is_ipv6_has_internet || ! is_dhcpv6_or_slaac || should_disable_accept_ra || should_disable_autoconf; then\n        return 1\n    fi\n    get_netconf_to slaac\n    # shellcheck disable=SC2154\n    [ \"$slaac\" = 1 ]\n}\n\nis_dhcpv6() {\n    # 如果是静态（包括自动获取到 IP 但无法联网而切换成静态）直接返回 1，不考虑 ra\n    # 防止部分机器slaac/dhcpv6获取的ip/网关无法上网\n\n    # 有可能 ra 的 dhcpv6/slaac 是打开的，但实测无法获取到 ipv6 地址\n    # is_dhcpv6_or_slaac 是实测结果，因此如果实测不通过，也返回 1\n\n    # 不要判断 is_staticv6，因为这会导致死循环\n    if ! is_ipv6_has_internet || ! is_dhcpv6_or_slaac || should_disable_accept_ra || should_disable_autoconf; then\n        return 1\n    fi\n    get_netconf_to dhcpv6\n\n    # shellcheck disable=SC2154\n    # 甲骨文即使没有添加 IPv6 地址，RA DHCPv6 标志也是开的\n    # 部分系统开机需要等 DHCPv6 超时\n    # 这种情况需要禁用 DHCPv6\n    if [ \"$dhcpv6\" = 1 ] && ! ip -6 -o addr show scope global dev \"$ethx\" | grep -q .; then\n        echo 'DHCPv6 flag is on, but DHCPv6 is not working.'\n        return 1\n    fi\n\n    [ \"$dhcpv6\" = 1 ]\n}\n\nis_have_ipv6() {\n    is_slaac || is_dhcpv6 || is_staticv6\n}\n\nis_enable_other_flag() {\n    get_netconf_to other\n    # shellcheck disable=SC2154\n    [ \"$other\" = 1 ]\n}\n\nis_have_rdnss() {\n    # rdnss 可能有几个\n    get_netconf_to rdnss\n    [ -n \"$rdnss\" ]\n}\n\n# dd 完检测到镜像是 windows 时会改写此方法\nis_windows() {\n    [ \"$distro\" = windows ]\n}\n\n# 15063 或之后才支持 rdnss\nis_windows_support_rdnss() {\n    [ \"$build_ver\" -ge 15063 ]\n}\n\nget_windows_version_from_windows_drive() {\n    local os_dir=$1\n\n    # https://wiki.tcl-lang.org/page/Windows+OS+name\n    # https://nsis.sourceforge.io/Get_Windows_version\n\n    # win10+ 才有 CurrentMajorVersionNumber 和 CurrentMinorVersionNumber\n    # CurrentVersion            6.3\n    # CurrentMajorVersionNumber  10\n    # CurrentMinorVersionNumber   0\n\n    apk add hivex\n    hive=$(find_file_ignore_case $os_dir/Windows/System32/config/SOFTWARE)\n\n    get_current_version_key() {\n        hivexget \"$hive\" \"Microsoft\\Windows NT\\CurrentVersion\" \"$1\"\n    }\n\n    # nt_ver\n    if { nt_ver_major=$(get_current_version_key CurrentMajorVersionNumber) &&\n        nt_ver_minor=$(get_current_version_key CurrentMinorVersionNumber); } 2>/dev/null; then\n        nt_ver=\"$nt_ver_major.$nt_ver_minor\"\n    else\n        # en_windows_vista_sp2_x64_dvd_342267.iso\n        # 安装前 CurrentVersion 是 6.0\n        # 安装后 CurrentVersion 是 6.0\n\n        # en_windows_vista_sp2_with_update_6003.23713_aio_7in1_x64_v26.01.13_by_adguard.iso\n        # 安装前 CurrentVersion 是 6.0.6002.18005\n        # 安装后 CurrentVersion 是 6.0\n\n        # 添加 cut 用于兼容这两种情况\n        nt_ver=$(get_current_version_key CurrentVersion | cut -d. -f1-2)\n    fi\n\n    # build_ver\n    # win10 22h2 19045 的 exe/dll 版本还是 19041 的，因此要从注册表获取\n    # vista sp2 iso 安装 KB4474419 后, CurrentBuild 是 6002, CurrentBuildNumber 是 6003\n    build_ver=$(get_current_version_key CurrentBuildNumber)\n\n    # rev_ver\n    # 实测 win10 winver 是从 UBR 读取 revision 版本\n    # vista sp2 iso 没有 UBR，后期有月度汇总更新包时才有 UBR\n    if ! rev_ver=$(get_current_version_key UBR 2>/dev/null); then\n        rev_ver=$(get_current_version_key BuildLabEx | cut -d. -f2)\n    fi\n\n    echo \"Version: $nt_ver.$build_ver.$rev_ver\" >&2\n    apk del hivex\n}\n\nis_elts() {\n    [ -n \"$elts\" ] && [ \"$elts\" = 1 ]\n}\n\nis_need_set_ssh_keys() {\n    [ -s /configs/ssh_keys ]\n}\n\nis_need_change_ssh_port() {\n    [ -n \"$ssh_port\" ] && ! [ \"$ssh_port\" = 22 ]\n}\n\nis_need_change_rdp_port() {\n    [ -n \"$rdp_port\" ] && ! [ \"$rdp_port\" = 3389 ]\n}\n\nis_need_manual_set_dnsv6() {\n    # 有没有可能是静态但是有 rdnss？\n    ! is_have_ipv6 && return $FALSE\n    is_dhcpv6 && return $FALSE\n    is_staticv6 && return $TRUE\n    is_slaac && ! is_enable_other_flag &&\n        { ! is_have_rdnss || { is_have_rdnss && is_windows && ! is_windows_support_rdnss; }; }\n}\n\nget_current_dns() {\n    mark=$(\n        case \"$1\" in\n        4) echo . ;;\n        6) echo : ;;\n        esac\n    )\n    # debian 11 initrd 没有 xargs awk\n    # debian 12 initrd 没有 xargs\n    if false; then\n        grep '^nameserver' /etc/resolv.conf | awk '{print $2}' | grep -F \"$mark\" | cut -d '%' -f1\n    else\n        grep '^nameserver' /etc/resolv.conf | cut -d' ' -f2 | grep -F \"$mark\" | cut -d '%' -f1\n    fi\n}\n\nto_upper() {\n    tr '[:lower:]' '[:upper:]'\n}\n\nto_lower() {\n    tr '[:upper:]' '[:lower:]'\n}\n\ndel_cr() {\n    sed 's/\\r$//'\n}\n\ndel_comment_lines() {\n    sed '/^[[:space:]]*#/d'\n}\n\ndel_empty_lines() {\n    sed '/^[[:space:]]*$/d'\n}\n\ndel_head_empty_lines_inplace() {\n    # 从第一行直到找到 ^[:space:]\n    # 这个区间内删除所有空行\n    sed -i '1,/[^[:space:]]/ { /^[[:space:]]*$/d }' \"$@\"\n}\n\nget_part_num_by_part() {\n    dev_part=$1\n    echo \"$dev_part\" | grep -o '[0-9]*' | tail -1\n}\n\nget_fallback_efi_file_name() {\n    case $(arch) in\n    x86_64) echo bootx64.efi ;;\n    aarch64) echo bootaa64.efi ;;\n    *) error_and_exit ;;\n    esac\n}\n\ndel_invalid_efi_entry() {\n    info \"del invalid EFI entry\"\n    apk add lsblk efibootmgr\n\n    efibootmgr --quiet --remove-dups\n\n    while read -r line; do\n        part_uuid=$(echo \"$line\" | awk -F ',' '{print $3}')\n        efi_index=$(echo \"$line\" | grep_efi_index)\n        if ! lsblk -o PARTUUID | grep -q \"$part_uuid\"; then\n            echo \"Delete invalid EFI Entry: $line\"\n            efibootmgr --quiet --bootnum \"$efi_index\" --delete-bootnum\n        fi\n    done < <(efibootmgr | grep 'HD(.*,GPT,')\n}\n\n# reinstall.sh 有同名方法\ngrep_efi_index() {\n    awk '{print $1}' | sed -e 's/Boot//' -e 's/\\*//'\n}\n\n# 某些机器可能不会回落到 bootx64.efi\n# 阿里云 ECS 启动项有 EFI Shell\n# 添加 bootx64.efi 到最后的话，会进入 EFI Shell\n# 因此添加到最前面\nadd_default_efi_to_nvram() {\n    info \"add default EFI to nvram\"\n\n    apk add lsblk efibootmgr\n\n    if efi_row=$(lsblk /dev/$xda -ro NAME,PARTTYPE,PARTUUID | grep -i \"$EFI_UUID\"); then\n        efi_part_uuid=$(echo \"$efi_row\" | awk '{print $3}')\n        efi_part_name=$(echo \"$efi_row\" | awk '{print $1}')\n        efi_part_num=$(get_part_num_by_part \"$efi_part_name\")\n        efi_file=$(get_fallback_efi_file_name)\n\n        # 创建条目，先判断是否已经存在\n        # 好像没必要先判断\n        if true || ! efibootmgr | grep -i \"HD($efi_part_num,GPT,$efi_part_uuid,.*)/File(\\\\\\EFI\\\\\\boot\\\\\\\\$efi_file)\"; then\n            efibootmgr --create \\\n                --disk \"/dev/$xda\" \\\n                --part \"$efi_part_num\" \\\n                --label \"$efi_file\" \\\n                --loader \"\\\\EFI\\\\boot\\\\$efi_file\"\n        fi\n    else\n        # shellcheck disable=SC2154\n        if [ \"$confirmed_no_efi\" = 1 ]; then\n            echo 'Confirmed no EFI in previous step.'\n        else\n            # reinstall.sh 里确认过一遍，但是逻辑扇区大于 512 时，可能漏报？\n            # 这里的应该会根据逻辑扇区来判断？\n            echo \"\nWarning: This machine is currently using EFI boot, but the main hard drive does not have an EFI partition.\nIf this machine supports Legacy BIOS boot (CSM), you can safely restart into the new system by running the reboot command.\nIf this machine does not support Legacy BIOS boot (CSM), you will not be able to enter the new system after rebooting.\n\n警告：本机目前使用 EFI 引导，但主硬盘没有 EFI 分区。\n如果本机支持 Legacy BIOS 引导 (CSM)，你可以运行 reboot 命令安全地重启到新系统。\n如果本机不支持 Legacy BIOS 引导 (CSM)，重启后将无法进入新系统。\n\"\n            exit\n        fi\n    fi\n}\n\nunix2dos() {\n    target=$1\n\n    # 先原地unix2dos，出错再用cat，可最大限度保留文件权限\n    if ! command unix2dos $target 2>/tmp/unix2dos.log; then\n        # 出错后删除 unix2dos 创建的临时文件\n        rm \"$(awk -F: '{print $2}' /tmp/unix2dos.log | xargs)\"\n        tmp=$(mktemp)\n        cp $target $tmp\n        command unix2dos $tmp\n        # cat 可以保留权限\n        cat $tmp >$target\n        rm $tmp\n    fi\n}\n\ninsert_into_file() {\n    local file=$1\n    local location=$2\n    local regex_to_find=$3\n    shift 3\n\n    if ! [ -f \"$file\" ]; then\n        error_and_exit \"File not found: $file\"\n    fi\n\n    # 默认 grep -E\n    if [ $# -eq 0 ]; then\n        set -- -E\n    fi\n\n    if [ \"$location\" = head ]; then\n        bak=$(mktemp)\n        cp $file $bak\n        cat - $bak >$file\n    else\n        line_num=$(grep \"$@\" -n \"$regex_to_find\" \"$file\" | cut -d: -f1)\n\n        found_count=$(echo \"$line_num\" | wc -l)\n        if [ ! \"$found_count\" -eq 1 ]; then\n            return 1\n        fi\n\n        case \"$location\" in\n        before) line_num=$((line_num - 1)) ;;\n        after) ;;\n        *) return 1 ;;\n        esac\n\n        sed -i \"${line_num}r /dev/stdin\" \"$file\"\n    fi\n}\n\nget_eths() {\n    (\n        cd /dev/netconf\n        ls\n    )\n}\n\nis_distro_like_debian() {\n    [ \"$distro\" = debian ] || [ \"$distro\" = kali ]\n}\n\ncreate_ifupdown_config() {\n    conf_file=$1\n\n    rm -f $conf_file\n\n    if is_distro_like_debian; then\n        cat <<EOF >>$conf_file\nsource /etc/network/interfaces.d/*\n\nEOF\n    fi\n\n    # 生成 lo配置\n    cat <<EOF >>$conf_file\nauto lo\niface lo inet loopback\nEOF\n\n    # ethx\n    for ethx in $(get_eths); do\n        mode=auto\n        # shellcheck disable=SC2154\n        if false; then\n            if { [ \"$distro\" = debian ] && [ \"$releasever\" -ge 12 ]; } ||\n                [ \"$distro\" = kali ]; then\n                # alice + allow-hotplug 会有问题\n                # 问题 1 debian 9/10/11/12:\n                # 如果首次启动时，/etc/networking/interfaces 的 ethx 跟安装时不同\n                # 即使启动 networking 服务前成功执行了 fix-eth-name.sh ，网卡也不会启动\n                # 测试方法: 安装时手动修改 /etc/networking/interfaces enp3s0 为其他名字\n                # 问题 2 debian 9/10/11:\n                # 重启系统后会自动启动网卡，但运行 systemctl restart networking 会关闭网卡\n                # 可能的原因: /lib/systemd/system/networking.service 没有 hotplug 相关内容，而 debian 12+ 有\n                if [ -f /etc/network/devhotplug ] && grep -wo \"$ethx\" /etc/network/devhotplug; then\n                    mode=allow-hotplug\n                fi\n            fi\n\n            # if is_have_cmd udevadm; then\n            #     enpx=$(udevadm test-builtin net_id /sys/class/net/$ethx 2>&1 | grep ID_NET_NAME_PATH= | cut -d= -f2)\n            # fi\n        fi\n\n        # dmit debian 普通内核和云内核网卡名不一致，因此需要 rename\n        # 安装系统时 ens18\n        # 普通内核   ens18\n        # 云内核     enp6s18\n        # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=928923\n\n        # 头部\n        get_netconf_to mac_addr\n        {\n            echo\n            # 这是标记，fix-eth-name 要用，不要删除\n            # shellcheck disable=SC2154\n            echo \"# mac $mac_addr\"\n            echo $mode $ethx\n        } >>$conf_file\n\n        # ipv4\n        if is_dhcpv4; then\n            echo \"iface $ethx inet dhcp\" >>$conf_file\n\n        elif is_staticv4; then\n            get_netconf_to ipv4_addr\n            get_netconf_to ipv4_gateway\n            cat <<EOF >>$conf_file\niface $ethx inet static\n    address $ipv4_addr\n    gateway $ipv4_gateway\nEOF\n            # dns\n            if list=$(get_current_dns 4); then\n                for dns in $list; do\n                    cat <<EOF >>$conf_file\n    dns-nameservers $dns\nEOF\n                done\n            fi\n        fi\n\n        # ipv6\n        if is_slaac; then\n            echo \"iface $ethx inet6 auto\" >>$conf_file\n\n        elif is_dhcpv6; then\n            # debian 13 使用 ifupdown + dhcpcd-base\n            # inet/inet6 都配置成 dhcp 时，重启后 dhcpv4 会丢失\n            # 手动 systemctl restart networking 后正常\n            # 删除 dhcpcd-base 安装 isc-dhcp-client（类似 debian 12 升级到 13），轮到 dhcpv6 丢失\n            if { [ \"$distro\" = debian ] && [ \"$releasever\" -ge 13 ]; } ||\n                [ \"$distro\" = kali ]; then\n                echo \"iface $ethx inet6 auto\" >>$conf_file\n            else\n                echo \"iface $ethx inet6 dhcp\" >>$conf_file\n            fi\n\n        elif is_staticv6; then\n            get_netconf_to ipv6_addr\n            get_netconf_to ipv6_gateway\n            cat <<EOF >>$conf_file\niface $ethx inet6 static\n    address $ipv6_addr\n    gateway $ipv6_gateway\nEOF\n            # debian 9\n            # ipv4 支持静态 onlink 网关\n            # ipv6 不支持静态 onlink 网关，需使用 post-up 添加，未测试动态\n            # ipv6 也不支持直接 ip route add default via xxx onlink\n            if [ \"$distro\" = debian ] && [ \"$releasever\" -le 9 ]; then\n                # debian 添加 gateway 失败时不会执行 post-up\n                # 因此 gateway post-up 只能二选一\n\n                # 注释最后一行，也就是 gateway\n                sed -Ei '$s/^( *)/\\1# /' \"$conf_file\"\n                cat <<EOF >>$conf_file\n    post-up ip route add $ipv6_gateway dev $ethx\n    post-up ip route add default via $ipv6_gateway dev $ethx\nEOF\n            fi\n\n            # 额外的 IPv6 地址（子网不含网关的地址）\n            get_netconf_to ipv6_extra_addrs\n            if [ -n \"$ipv6_extra_addrs\" ]; then\n                (\n                    IFS=','\n                    for _addr in $ipv6_extra_addrs; do\n                        echo \"    post-up ip -6 addr add $_addr dev $ethx\" >>$conf_file\n                    done\n                )\n            fi\n        fi\n\n        # dns\n        # 有 ipv6 但需设置 dns 的情况\n        if is_need_manual_set_dnsv6; then\n            for dns in $(get_current_dns 6); do\n                cat <<EOF >>$conf_file\n    dns-nameserver $dns\nEOF\n            done\n        fi\n\n        # 禁用 ra\n        if should_disable_accept_ra; then\n            if [ \"$distro\" = alpine ]; then\n                cat <<EOF >>$conf_file\n    pre-up echo 0 >/proc/sys/net/ipv6/conf/$ethx/accept_ra\nEOF\n            else\n                cat <<EOF >>$conf_file\n    accept_ra 0\nEOF\n            fi\n        fi\n\n        # 禁用 autoconf\n        if should_disable_autoconf; then\n            if [ \"$distro\" = alpine ]; then\n                cat <<EOF >>$conf_file\n    pre-up echo 0 >/proc/sys/net/ipv6/conf/$ethx/autoconf\nEOF\n            else\n                cat <<EOF >>$conf_file\n    autoconf 0\nEOF\n            fi\n        fi\n    done\n}\n\nnewline_to_comma() {\n    tr '\\n' ','\n}\n\nspace_to_newline() {\n    sed 's/ /\\n/g'\n}\n\ntrim() {\n    sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'\n}\n\nquote_word() {\n    sed -E 's/([^[:space:]]+)/\"\\1\"/g'\n}\n\nquote_line() {\n    awk '{print \"\\\"\"$0\"\\\"\"}'\n}\n\nadd_space() {\n    space_count=$1\n\n    spaces=$(printf '%*s' \"$space_count\" '')\n    sed \"s/^/$spaces/\"\n}\n\n# 不够严谨，谨慎使用\nnix_replace() {\n    local key=$1\n    local value=$2\n    local type=$3\n    local file=$4\n    local key_ value_\n\n    key_=$(echo \"$key\" | sed 's \\. \\\\\\. g') # . 改成 \\.\n\n    if [ \"$type\" = array ]; then\n        local value_=\"[ $value ]\"\n    fi\n\n    sed -i \"s/$key_ =.*/$key = $value_;/\" \"$file\"\n}\n\ncreate_nixos_network_config() {\n    conf_file=$1\n    true >$conf_file\n\n    # 头部\n    cat <<EOF >>$conf_file\nnetworking = {\n  usePredictableInterfaceNames = false;\nEOF\n\n    for ethx in $(get_eths); do\n        # ipv4\n        if is_staticv4; then\n            get_netconf_to ipv4_addr\n            get_netconf_to ipv4_gateway\n            IFS=/ read -r address prefix < <(echo \"$ipv4_addr\")\n            cat <<EOF >>$conf_file\n  interfaces.$ethx.ipv4.addresses = [\n    {\n      address = \"$address\";\n      prefixLength = $prefix;\n    }\n  ];\n  defaultGateway = {\n    address = \"$ipv4_gateway\";\n    interface = \"$ethx\";\n  };\nEOF\n        fi\n\n        # ipv6\n        if is_staticv6; then\n            get_netconf_to ipv6_addr\n            get_netconf_to ipv6_gateway\n            IFS=/ read -r address prefix < <(echo \"$ipv6_addr\")\n            cat <<EOF >>$conf_file\n  interfaces.$ethx.ipv6.addresses = [\n    {\n      address = \"$address\";\n      prefixLength = $prefix;\n    }\n  ];\n  defaultGateway6 = {\n    address = \"$ipv6_gateway\";\n    interface = \"$ethx\";\n  };\nEOF\n        fi\n    done\n\n    # 全局 dns\n    need_set_dns=false\n    for ethx in $(get_eths); do\n        if is_staticv4 || is_staticv6 || is_need_manual_set_dnsv6; then\n            need_set_dns=true\n            break\n        fi\n    done\n\n    if $need_set_dns; then\n        cat <<EOF >>$conf_file\n  nameservers = [\n$(get_current_dns | quote_line | add_space 4)\n  ];\nEOF\n    fi\n\n    # 尾部\n    cat <<EOF >>$conf_file\n};\nEOF\n\n    # nixos 默认网络管理器是 dhcpcd\n    # 但配置静态 ip 时用的是脚本\n    # /nix/store/qcr1xxjdxcrnwqwrgysqpxx2aibp9fdl-unit-script-network-addresses-eth0-start/bin/network-addresses-eth0-start\n    # ...\n    # if out=$(ip addr replace \"181.x.x.x/24\" dev \"eth0\" 2>&1); then\n    #   echo \"done\"\n    # else\n    #   echo \"'ip addr replace \"181.x.x.x/24\" dev \"eth0\"' failed: $out\"\n    #   exit 1\n    # fi\n    # ...\n\n    # 禁用 ra/autoconf\n    local mode=1\n    for ethx in $(get_eths); do\n        if should_disable_accept_ra; then\n            case \"$mode\" in\n            1)\n                cat <<EOF >>$conf_file\nboot.kernel.sysctl.\"net.ipv6.conf.$ethx.accept_ra\" = false;\nEOF\n                ;;\n            2)\n                # nixos 配置静态 ip 时用的是脚本\n                # 好像因此不起作用\n                cat <<EOF >>$conf_file\nnetworking.dhcpcd.extraConfig =\n  ''\n    interface $ethx\n      ipv6ra_noautoconf\n  '';\nEOF\n                ;;\n            3)\n                # 暂时没用到 networkd\n                cat <<EOF >>$conf_file\nsystemd.network.networks.$ethx = {\n   matchConfig.Name = \"$ethx\";\n   networkConfig = {\n     IPv6AcceptRA = false;\n   };\n };\nEOF\n                ;;\n            esac\n        fi\n\n        if should_disable_autoconf; then\n            case \"$mode\" in\n            1)\n                cat <<EOF >>$conf_file\nboot.kernel.sysctl.\"net.ipv6.conf.$ethx.autoconf\" = false;\nEOF\n                ;;\n            2) ;;\n            3) ;;\n            esac\n        fi\n    done\n}\n\ninstall_alpine() {\n    info \"install alpine\"\n\n    need_ram=512\n    swap_size=$(get_need_swap_size $need_ram)\n    [ \"$swap_size\" -gt 0 ] && hack_lowram=true || hack_lowram=false\n\n    # alpine 安装时会自动检测安装需要的 firmware\n    # https://github.com/alpinelinux/alpine-conf/blob/3.18.1/setup-disk.in#L421\n    # 但如果没有 modloop 则无法检测\n    # 所以删除 modloop 前先记录用到的 firmware 包\n    fw_pkgs=$(get_alpine_firmware_pkgs)\n\n    if $hack_lowram; then\n        # 预先加载需要的模块\n        if rc-service -q modloop status; then\n            modules=\"ext4 vfat nls_utf8 nls_cp437\"\n            for mod in $modules; do\n                modprobe $mod\n            done\n            # crc32c 等于 crc32c-intel\n            # 没有 sse4.2 的机器加载 crc32c 时会报错 modprobe: ERROR: could not insert 'crc32c_intel': No such device\n            modprobe crc32c || modprobe crc32c-generic\n        fi\n\n        # 删除 modloop ，释放内存\n        ensure_service_stopped modloop\n        rm -f /lib/modloop-lts /lib/modloop-virt\n    fi\n\n    # bios机器用 setup-disk 自动分区会有 boot 分区\n    # 因此手动分区安装\n    create_part\n    mount_part_basic_layout /os /os/boot/efi\n\n    # 创建 swap\n    if $hack_lowram; then\n        create_swap $swap_size /os/swapfile\n    fi\n\n    # 网络配置\n    create_ifupdown_config /etc/network/interfaces\n    echo\n    cat -n /etc/network/interfaces\n    echo\n\n    # 在 arm netboot initramfs init 中\n    # 如果识别到rtc硬件，就往系统添加hwclock服务，否则添加swclock\n    # 这个设置也被复制到安装的系统中\n    # 但是从initramfs chroot到真正的系统后，是能识别rtc硬件的\n    # 所以我们手动改用hwclock修复这个问题\n    rc-update del swclock boot || true\n    rc-update add hwclock boot\n\n    # 通过 setup-alpine 安装会启用以下几个服务\n    # https://github.com/alpinelinux/alpine-conf/blob/3.18.1/setup-alpine.in#L229\n\n    # boot\n    rc-update add networking boot\n    rc-update add seedrng boot\n\n    # default\n    rc-update add crond\n    if [ -e /dev/input/event0 ]; then\n        rc-update add acpid\n    fi\n\n    # 如果是 vm 就用 virt 内核\n    if is_virt; then\n        kernel_flavor=\"virt\"\n    else\n        kernel_flavor=\"lts\"\n    fi\n\n    # 重置为官方仓库配置\n    # 国内机可能无法访问mirror列表而报错\n    if false; then\n        true >/etc/apk/repositories\n        setup-apkrepos -1\n    fi\n\n    # setup-disk 安装 grub 跳过了添加引导项到 nvram\n    # 防止部分机器不会 fallback 到 bootx64.efi\n    if is_efi; then\n        apk add efibootmgr\n        sed -i 's/--no-nvram//' \"$(which setup-disk)\"\n    fi\n\n    # 安装到硬盘\n    # alpine默认使用 syslinux (efi 环境除外)，这里强制使用 grub，方便用脚本再次重装\n    KERNELOPTS=\"$(get_ttys console=)\"\n    export KERNELOPTS\n    export BOOTLOADER=\"grub\"\n    setup-disk -m sys -k $kernel_flavor /os\n\n    # 删除 setup-disk 时自动安装的包\n    apk del e2fsprogs dosfstools efibootmgr grub*\n\n    # 如果没有挂载 /proc\n\n    # 1. chroot /os setup-keymap us us 会报错\n    # grep: /proc/filesystems: No such file or directory\n\n    # 2. 安装固件微码会触发 grub-probe，如果没挂载会报错\n    # Executing grub-2.12-r5.trigger\n    # /usr/sbin/grub-probe: error: failed to get canonical path of `/dev/vda1'.\n    # ERROR: grub-2.12-r5.trigger: script exited with error 1\n\n    mount_pseudo_fs /os\n\n    # 安装到硬盘后才安装各种应用\n    # 避免占用 Live OS 内存\n\n    # 网络\n    # udhcpc\n    # 坑1 ip -4 addr 无法知道是否是 dhcp\n    # 坑2 networking 服务不会运行 udhcpc6\n    # 坑3 h3c 移动云电脑 udhcpc6 无法获取 dhcpv6\n\n    # dhcpcd\n    # 坑1 slaac默认开了隐私保护，造成ip和后台面板不一致\n\n    # slaac方案1: udhcpc + rdnssd\n    # slaac方案2: dhcpcd + 关闭隐私保护\n    # dhcpv6方案: dhcpcd\n\n    # 综合使用dhcpcd方案\n    # 1 无需改动/etc/network/interfaces，自动根据ra使用slaac和dhcpv6\n    # 2 自带rdnss支持\n    # 3 唯一要做的是关闭隐私保护\n\n    # 安装 dhcpcd\n    chroot /os apk add dhcpcd\n    chroot /os sed -i '/^slaac private/s/^/#/' /etc/dhcpcd.conf\n    chroot /os sed -i '/^#slaac hwaddr/s/^#//' /etc/dhcpcd.conf\n\n    # 安装其他部件\n    chroot /os setup-keymap us us\n    chroot /os setup-timezone -i Asia/Shanghai\n    # 3.21 默认是 chrony\n    # 3.22 默认是 busybox ntp\n    printf '\\n' | chroot /os setup-ntp || true\n\n    # 设置公钥\n    if is_need_set_ssh_keys; then\n        set_ssh_keys_and_del_password /os\n    fi\n\n    # 下载 fix-eth-name\n    download \"$confhome/fix-eth-name.sh\" /os/fix-eth-name.sh\n    download \"$confhome/fix-eth-name.initd\" /os/etc/init.d/fix-eth-name\n    chmod +x /os/etc/init.d/fix-eth-name\n    chroot /os rc-update add fix-eth-name boot\n\n    # 安装 frpc\n    if ls /configs/frpc.* >/dev/null 2>&1; then\n        chroot /os apk add frp\n        # chroot rc-update add 默认添加到 sysinit\n        # 但不加 chroot 默认添加到 default\n        chroot /os rc-update add frpc boot\n        cp -f /configs/frpc.* /os/etc/frp/\n    fi\n\n    # setup-disk 会自动选择固件，但不包括微码？\n    # https://github.com/alpinelinux/alpine-conf/blob/3.18.1/setup-disk.in#L421\n    if fw_pkgs=\"$fw_pkgs $(get_ucode_firmware_pkgs)\" && [ -n \"$fw_pkgs\" ]; then\n        chroot /os apk add $fw_pkgs\n    fi\n\n    # 3.19 或以上，非 efi 需要手动安装 grub\n    if ! is_efi; then\n        chroot /os grub-install --target=i386-pc /dev/$xda\n    fi\n\n    # efi grub 添加 fwsetup 条目\n    chroot /os update-grub\n\n    # 是否保留 swap\n    if [ -e /os/swapfile ]; then\n        if false; then\n            echo \"/swapfile swap swap defaults 0 0\" >>/os/etc/fstab\n            ln -sf /etc/init.d/swap /os/etc/runlevels/boot/swap\n        else\n            swapoff -a\n            rm /os/swapfile\n        fi\n    fi\n}\n\nget_cpu_vendor() {\n    cpu_vendor=$(grep 'vendor_id' /proc/cpuinfo | head -1 | awk '{print $NF}')\n    case \"$cpu_vendor\" in\n    GenuineIntel) echo intel ;;\n    AuthenticAMD) echo amd ;;\n    *) echo other ;;\n    esac\n}\n\nmin() {\n    printf \"%d\\n\" \"$@\" | sort -n | head -n 1\n}\n\n# 设置线程\n# 根据 cpu 核数，每个线程的内存，取最小值\nget_build_threads() {\n    threads_per_mb=$1\n\n    threads_by_core=$(nproc)\n    threads_by_ram=$(($(get_approximate_ram_size) / threads_per_mb))\n    [ $threads_by_ram -eq 0 ] && threads_by_ram=1\n    min $threads_by_ram $threads_by_core\n}\n\nadd_newline() {\n    # shellcheck disable=SC1003\n    case \"$1\" in\n    head | start) sed -e '1s/^/\\n/' ;;\n    tail | end) sed -e '$a\\\\' ;;\n    both) sed -e '1s/^/\\n/' -e '$a\\\\' ;;\n    esac\n}\n\ninstall_nixos() {\n    info \"Install NixOS\"\n\n    os_dir=/os\n    keep_swap=true\n    nix_from=website\n    ram_per_thread=2048\n\n    threads=$(get_build_threads $ram_per_thread)\n    swap_size=$(get_need_swap_size $ram_per_thread)\n\n    show_nixos_config() {\n        echo\n        # 过滤 frp auth.token\n        cat -n /os/etc/nixos/configuration.nix | grep -Fv 'auth.token'\n        echo\n        cat -n /os/etc/nixos/hardware-configuration.nix\n        echo\n    }\n\n    # 挂载分区，创建 swapfile\n    mount_part_basic_layout /os /os/efi\n    if [ \"$swap_size\" -gt 0 ]; then\n        create_swap \"$swap_size\" /os/swapfile\n    fi\n\n    # 步骤\n    # 1. 安装 nix (nix-xxx)\n    # 2. 用 nix 安装 nixos-install-tools (nixos-xxx)\n    # 3. 运行 nixos-generate-config 生成配置 + 编辑\n    # 4. 运行 nixos-install\n    # https://nixos.org/manual/nixos/stable/index.html#sec-installing-from-other-distro\n\n    # nix 安装方式                                    分支          版本\n    # apk add nix                                    3.20         2.22.0  # nix 本体跟 alpine 正常的软件一样，不在 /nix/store 里面\n    # env -iA nixpkgs.nix                            24.05        2.18.5\n    # sh <(curl -L https://nixos.org/nix/install)   unstable?     2.24.2\n\n    # apk add 安装的 nix 有时会卡在\n    # copying path '/nix/store/gcbrjlfm5h21ybf1h2lfq773zafjmzjr-curl-8.7.1-man' from 'https://cache.nixos.org'...\n    # 但是 cpu 空载\n\n    # 安装 nix\n    mkdir -p /os/nix /nix\n    mount --bind /os/nix /nix\n\n    # nix 安装脚本和 /root/.nix-profile/etc/profile.d/nix.sh 都会用到这两个变量\n    # 但从 alpine local.d 运行没有这两个变量\n    export USER=root\n    export HOME=/root\n\n    case \"$nix_from\" in\n    alpine)\n        apk add nix\n        # 设置 nix 镜像和线程\n        # alpine 默认设置了 4 线程\n        # https://gitlab.alpinelinux.org/alpine/aports/-/blob/master/community/nix/APKBUILD#L125\n        sed -i '/max-jobs/d' /etc/nix/nix.conf\n        echo \"max-jobs = $threads\" >>/etc/nix/nix.conf\n        if is_in_china; then\n            echo \"substituters = $mirror/store\" >>/etc/nix/nix.conf\n        fi\n        rc-service -q nix-daemon restart\n        # 添加 nix-env 安装的软件到 PATH\n        PATH=\"/root/.nix-profile/bin:$PATH\"\n        ;;\n    website)\n        # https://gitlab.alpinelinux.org/alpine/aports/-/blob/master/community/nix/nix.pre-install\n        # https://nix.dev/manual/nix/latest/installation/multi-user\n        if ! grep -q nixbld /etc/passwd; then\n            addgroup -S nixbld\n            for n in $(seq 1 10); do\n                adduser -S -D -H -h /var/empty -s /sbin/nologin -G nixbld \\\n                    -g \"Nix build user $n\" nixbld$n\n            done\n        fi\n\n        # 备用方案\n        # 1. 从 https://mirror.nju.edu.cn/nix-channels/nixos-25.11/nixexprs.tar.xz 获取\n        #    https://github.com/NixOS/nixpkgs/blob/nixos-25.11/pkgs/tools/package-management/nix/default.nix\n        #    https://github.com/NixOS/nixpkgs/blob/nixos-25.11/nixos/modules/installer/tools/nix-fallback-paths.nix\n        # 2. 安装最新版 nix，添加 nixos channel 后获取\n        #    nix eval -f '<nixpkgs>' --raw 'nixVersions.stable.version' --extra-experimental-features nix-command\n\n        if true; then\n            # nix 版本号使用目标系统里面的\n            download $mirror/nixos-$releasever/store-paths.xz /os/store-paths.xz\n            apk add xz\n            nix_ver=$(xz -dc </os/store-paths.xz | grep -F 'vm-test-run-nix-upgrade' |\n                head -1 | awk -F- '{print $7}' | grep .)\n            rm -f /os/store-paths.xz\n            if is_in_china; then\n                sh_mirror=https://mirror.nju.edu.cn/nix\n            else\n                sh_mirror=https://releases.nixos.org/nix\n            fi\n            sh=$sh_mirror/nix-$nix_ver/install\n        else\n            # 最新版 nix 在 nixos-install 时可能会出问题\n            # https://github.com/bin456789/reinstall/issues/451\n            if is_in_china; then\n                sh=https://mirror.nju.edu.cn/nix/latest/install\n            else\n                sh=https://nixos.org/nix/install\n            fi\n        fi\n\n        apk add xz\n        wget -O- \"$sh\" | sh -s -- --no-daemon --no-channel-add\n        apk del xz\n        # shellcheck source=/dev/null\n        . /root/.nix-profile/etc/profile.d/nix.sh\n        ;;\n    esac\n\n    # 添加 channel\n    # shellcheck disable=SC2154\n    nix-channel --add $mirror/nixos-$releasever nixpkgs\n    nix-channel --update\n\n    # 安装 channal 的 nix\n    # shellcheck source=/dev/null\n    if false; then\n        nix-env -iA nixpkgs.nix -j $threads\n        . ~/.nix-profile/etc/profile.d/nix.sh\n    fi\n\n    # 安装 nixos-install-tools\n    nix-env -iA nixpkgs.nixos-install-tools -j $threads\n\n    # 生成配置并显示\n    nixos-generate-config --root /os\n    echo \"Original NixOS Configuration:\"\n    show_nixos_config\n\n    # 修改 configuration.nix\n    if is_efi; then\n        nix_bootloader=\"boot.loader.efi.efiSysMountPoint = \\\"/efi\\\";\"\n    else\n        nix_bootloader=\"boot.loader.grub.device = \\\"/dev/$xda\\\";\"\n    fi\n\n    if is_in_china; then\n        nix_substituters=\"nix.settings.substituters = lib.mkForce [ \\\"$mirror/store\\\" ];\"\n    fi\n\n    if [ -e /os/swapfile ] && $keep_swap; then\n        nix_swap=\"swapDevices = [{ device = \\\"/swapfile\\\"; size = $swap_size; }];\"\n    fi\n\n    if is_need_set_ssh_keys; then\n        nix_ssh_keys_or_PermitRootLogin=\"\nusers.users.root.openssh.authorizedKeys.keys = [\n$(del_comment_lines </configs/ssh_keys | del_empty_lines | quote_line | add_space 2)\n];\n\"\n    else\n        nix_ssh_keys_or_PermitRootLogin='services.openssh.settings.PermitRootLogin = \"yes\";'\n    fi\n\n    if is_need_change_ssh_port; then\n        nix_ssh_ports=\"services.openssh.ports = [ $ssh_port ];\"\n    fi\n\n    if ls /configs/frpc.* >/dev/null 2>&1; then\n        nix_frpc=$(\n            if false; then\n                # 原始 frpc.toml 转 toml 对象 ，再转成最终使用的 frpc.toml\n                # 可以避免原始 frpc.toml 有错误导致失联\n                # 但是 frpc 配置还支持 ini json yaml\n                # 因此不使用这个方法\n                cat <<EOF\nservices.frp = {\n  enable = true;\n  role = \"client\";\n  settings = builtins.fromTOML ''\n$(cat /configs/frpc.* | add_space 4)\n  '';\n};\nEOF\n            else\n                # 直接使用原始文件\n                (\n                    umask 077\n                    cp /configs/frpc.* /os/etc/nixos/\n                )\n                ext=$(basename /configs/frpc.* | awk -F. '{print $NF}')\n                cat <<EOF\nservices.frp = {\n  enable = true;\n  role = \"client\";\n};\nsystemd.services.frp.serviceConfig = {\n  LoadCredential = \"frpc.$ext:/etc/nixos/frpc.$ext\";\n  ExecStart = lib.mkForce \"\\${pkgs.frp}/bin/frpc -c \\\\\\${CREDENTIALS_DIRECTORY}/frpc.$ext\";\n};\nEOF\n            fi\n        )\n    fi\n\n    # TODO: 准确匹配网卡，添加 udev 或者直接配置 networkd 匹配 mac\n    create_nixos_network_config /tmp/nixos_network_config.nix\n\n    del_empty_lines <<EOF | add_space 2 | add_newline both |\n############### Add by reinstall.sh ###############\n$nix_bootloader\n$nix_swap\n$nix_substituters\nboot.kernelParams = [ $(get_ttys console= | quote_word) ];\nservices.openssh.enable = true;\n$nix_ssh_keys_or_PermitRootLogin\n$nix_ssh_ports\n$nix_frpc\n$(cat /tmp/nixos_network_config.nix)\n###################################################\nEOF\n        insert_into_file /os/etc/nixos/configuration.nix before \"networking.hostName\" -F\n\n    # 修改 hardware-configuration.nix\n    # 在 vultr efi 机器上，nixos-generate-config 不会添加 virtio_pci\n    # 导致 virtio_blk 用不了，启动时 initrd 找不到系统分区\n    # 可能由于 alpine 的 virtio_pci 编译进内核而不是模块\n    # 因此 nixos-generate-config 不会添加 virtio_pci 到配置文件\n    olds=$(\n        grep -F 'boot.initrd.availableKernelModules' /os/etc/nixos/hardware-configuration.nix |\n            cut -d= -f2 | tr -d '\"[];' | xargs\n    )\n    alls=\"$olds\"\n    # https://github.com/search?q=repo%3ANixOS%2Fnixpkgs+availableKernelModules&type=code\n    for mod in ahci ata_piix uhci_hcd sr_mod nvme \\\n        virtio_pci virtio_blk virtio_scsi \\\n        xen_blkfront xen_scsifront \\\n        hv_storvsc \\\n        vmw_pvscsi \\\n        mptspi; do\n        if [ -d /sys/module/$mod ] && ! echo \"$olds\" | grep -wq \"$mod\"; then\n            echo \"Adding modules: $mod\"\n            alls=\"$alls $mod\"\n        fi\n    done\n    # 去除多余的空格\n    alls=$(echo \"$alls\" | xargs)\n\n    # boot.initrd.availableKernelModules = [ \"ata_piix\" \"uhci_hcd\" \"virtio_pci\" \"sr_mod\" \"virtio_blk\" ];\n    nix_replace \\\n        boot.initrd.availableKernelModules \\\n        \"$(echo \"$alls\" | quote_word)\" \\\n        array \\\n        /os/etc/nixos/hardware-configuration.nix\n\n    # 显示修改后的配置\n    echo \"Modified NixOS Configuration:\"\n    show_nixos_config\n\n    # 安装系统\n    nixos-install --root /os --no-root-passwd -j $threads\n\n    # 设置密码\n    if ! is_need_set_ssh_keys; then\n        echo \"root:$(get_password_linux_sha512)\" | nixos-enter --root /os -- \\\n            /run/current-system/sw/bin/chpasswd -e\n    fi\n\n    # 设置 channel\n    if is_in_china; then\n        nixos-enter --root /os -- \\\n            /run/current-system/sw/bin/nix-channel --add $mirror/nixos-$releasever nixos\n    fi\n\n    # 清理\n    nix-env -e '*'\n    # /nix/var/nix/profiles/system/sw/bin/nix-collect-garbage -d\n    /nix/var/nix/profiles/system/sw/bin/nixos-enter --root /os -- \\\n        /run/current-system/sw/bin/nix-collect-garbage -d\n\n    # 删除 nix\n    umount /nix\n    apk del nix\n\n    # swapfile\n    if [ -e /os/swapfile ]; then\n        if $keep_swap; then\n            :\n        else\n            swapoff -a\n            rm -rf /os/swapfile\n        fi\n    fi\n\n    # 重新显示配置，方便查看\n    show_nixos_config\n}\n\nadd_systemd_service() {\n    local os_dir=$1\n    local service_name=$2\n\n    download \"$confhome/$service_name.service\" \"$os_dir/etc/systemd/system/$service_name.service\"\n    chroot \"$os_dir\" systemctl enable \"$service_name.service\"\n\n    # aosc 首次开机会执行 preset-all\n    # 因此需要设置 fix-eth-name 的 preset 状态\n    # 不然首次开机 /etc/systemd/system/multi-user.target.wants/fix-eth-name.service 会被删除\n    # 通常 /etc/systemd/system-preset/ 文件夹要新建，因此不放在这里\n\n    # 可能是 /usr/lib/systemd/system-preset/ 或者 /lib/systemd/system-preset/\n    if [ -d \"$os_dir/usr/lib/systemd/system-preset\" ]; then\n        echo \"enable $service_name.service\" >\"$os_dir/usr/lib/systemd/system-preset/01-$service_name.preset\"\n    else\n        echo \"enable $service_name.service\" >\"$os_dir/lib/systemd/system-preset/01-$service_name.preset\"\n    fi\n}\n\nadd_fix_eth_name_systemd_service() {\n    local os_dir=$1\n\n    # 无需执行 systemctl daemon-reload\n    # 因为 chroot 下执行会提示 Running in chroot, ignoring command 'daemon-reload'\n    download \"$confhome/fix-eth-name.sh\" \"$os_dir/fix-eth-name.sh\"\n    add_systemd_service \"$os_dir\" fix-eth-name\n}\n\nget_frpc_url() {\n    wget \"$confhome/get-frpc-url.sh\" -O- | sh -s \"$@\"\n}\n\nadd_frpc_systemd_service_if_need() {\n    local os_dir=$1\n\n    if ls /configs/frpc.* >/dev/null 2>&1; then\n        mkdir -p \"$os_dir/usr/local/bin\"\n        mkdir -p \"$os_dir/usr/local/etc/frpc\"\n\n        # 下载 frpc\n        # 注意下载的 frpc owner 不是 root:root\n        frpc_url=$(get_frpc_url linux)\n        basename=$(echo \"$frpc_url\" | awk -F/ '{print $NF}' | sed 's/\\.tar\\.gz//')\n        download \"$frpc_url\" \"$os_dir/frpc.tar.gz\"\n        # busybox tar 不支持 wildcard\n        # tar: */frpc: not found in archive\n        tar xzf \"$os_dir/frpc.tar.gz\" \"$basename/frpc\" -O >\"$os_dir/usr/local/bin/frpc\"\n        rm -f \"$os_dir/frpc.tar.gz\"\n        chmod a+x \"$os_dir/usr/local/bin/frpc\"\n\n        # frpc conf\n        cp -f /configs/frpc.* \"$os_dir/usr/local/etc/frpc/\"\n\n        # 添加服务\n        add_systemd_service \"$os_dir\" frpc\n    fi\n}\n\nbasic_init() {\n    os_dir=$1\n\n    # 此时不能用\n    # chroot $os_dir timedatectl set-timezone Asia/Shanghai\n    # Failed to create bus connection: No such file or directory\n\n    # debian 11 没有 systemd-firstboot\n    if is_have_cmd_on_disk $os_dir systemd-firstboot; then\n        if chroot $os_dir systemd-firstboot --help | grep -wq '\\--force'; then\n            chroot $os_dir systemd-firstboot --timezone=Asia/Shanghai --force\n        else\n            chroot $os_dir systemd-firstboot --timezone=Asia/Shanghai\n        fi\n    fi\n\n    # gentoo 不会自动创建 machine-id\n    clear_machine_id $os_dir\n\n    # sshd\n    chroot $os_dir ssh-keygen -A\n\n    sshd_enabled=false\n    sshs=\"sshd.service ssh.service sshd.socket ssh.socket\"\n    for i in $sshs; do\n        if chroot $os_dir systemctl -q is-enabled $i; then\n            sshd_enabled=true\n            break\n        fi\n    done\n    if ! $sshd_enabled; then\n        for i in $sshs; do\n            if chroot $os_dir systemctl -q enable $i; then\n                break\n            fi\n        done\n    fi\n\n    if is_need_change_ssh_port; then\n        change_ssh_port $os_dir $ssh_port\n    fi\n\n    # 公钥/密码\n    if is_need_set_ssh_keys; then\n        set_ssh_keys_and_del_password $os_dir\n    else\n        change_root_password $os_dir\n        allow_root_password_login $os_dir\n        allow_password_login $os_dir\n    fi\n\n    # 下载 fix-eth-name.service\n    # 即使开了 net.ifnames=0 也需要\n    # 因为 alpine live 和目标系统的网卡顺序可能不同\n    add_fix_eth_name_systemd_service $os_dir\n\n    # frpc\n    add_frpc_systemd_service_if_need $os_dir\n}\n\ninstall_arch_gentoo_aosc() {\n    info \"install $distro\"\n\n    network_app=$(\n        case \"$distro\" in\n        arch | gentoo) echo systemd-networkd ;;\n        aosc) echo network-manager ;;\n        esac\n    )\n\n    set_locale() {\n        echo \"C.UTF-8 UTF-8\" >>$os_dir/etc/locale.gen\n        chroot $os_dir locale-gen\n    }\n\n    # shellcheck disable=SC2317\n    install_arch() {\n        # 添加 swap\n        create_swap_if_ram_less_than 1024 $os_dir/swapfile\n\n        apk add arch-install-scripts\n\n        # 为了二次运行时 /etc/pacman.conf 未修改\n        if [ -f /etc/pacman.conf.orig ]; then\n            cp /etc/pacman.conf.orig /etc/pacman.conf\n        else\n            cp /etc/pacman.conf /etc/pacman.conf.orig\n        fi\n\n        # 设置 repo\n        insert_into_file /etc/pacman.conf before '\\[core\\]' <<EOF\nSigLevel = Never\nParallelDownloads = 5\nEOF\n        cat <<EOF >>/etc/pacman.conf\n[core]\nInclude = /etc/pacman.d/mirrorlist\n\n[extra]\nInclude = /etc/pacman.d/mirrorlist\nEOF\n        mkdir -p /etc/pacman.d\n        # shellcheck disable=SC2016\n        case \"$(uname -m)\" in\n        x86_64) dir='$repo/os/$arch' ;;\n        aarch64) dir='$arch/$repo' ;;\n        esac\n        # shellcheck disable=SC2154\n        echo \"Server = $mirror/$dir\" >/etc/pacman.d/mirrorlist\n\n        # 安装系统\n        # 要安装分区工具(包含 fsck.xxx)，用于 initramfs 检查分区数据\n        # base 包含 e2fsprogs\n        pkgs=\"base grub openssh\"\n        if is_efi; then\n            pkgs=\"$pkgs efibootmgr dosfstools\"\n        fi\n        if [ \"$(uname -m)\" = aarch64 ]; then\n            pkgs=\"$pkgs archlinuxarm-keyring\"\n        fi\n        pacstrap -K $os_dir $pkgs\n\n        # dns\n        cp_resolv_conf $os_dir\n\n        # 挂载伪文件系统\n        mount_pseudo_fs $os_dir\n\n        # 要先设置语言，再安装内核，不然出现\n        # ==> Creating gzip-compressed initcpio image: '/boot/initramfs-linux.img'\n        # bsdtar: bsdtar: Failed to set default locale\n        # Failed to set default locale\n        set_locale\n        if [ \"$(uname -m)\" = aarch64 ]; then\n            chroot $os_dir pacman-key --lsign-key builder@archlinuxarm.org\n        fi\n\n        # firmware + microcode\n        if fw_pkgs=$(get_ucode_firmware_pkgs) && [ -n \"$fw_pkgs\" ]; then\n            chroot $os_dir pacman -Syu --noconfirm $fw_pkgs\n        fi\n\n        # arm 的内核有多种选择，默认是 linux-aarch64，所以要添加 --noconfirm\n        chroot $os_dir pacman -Syu --noconfirm linux\n    }\n\n    # shellcheck disable=SC2317\n    install_gentoo() {\n        # 添加 swap\n        create_swap_if_ram_less_than 2048 $os_dir/swapfile\n\n        # 解压系统\n        apk add tar xz pv\n        # shellcheck disable=SC2154\n        download \"$img\" $os_dir/gentoo.tar.xz\n        echo \"Uncompressing Gentoo...\"\n        pv -f $os_dir/gentoo.tar.xz | tar xpJ --numeric-owner --xattrs-include='*.*' -C $os_dir\n        rm $os_dir/gentoo.tar.xz\n        apk del tar xz pv\n\n        # dns\n        cp_resolv_conf $os_dir\n\n        # 挂载伪文件系统\n        mount_pseudo_fs $os_dir\n\n        # 下载仓库，选择 profile\n        chroot $os_dir emerge-webrsync\n        profile=$(\n            # 筛选 stable systemd，再选择最短的\n            if false; then\n                chroot $os_dir eselect profile list | grep stable | grep systemd |\n                    awk '(NR == 1 || length($2) < length(shortest)) { shortest = $2 } END { print shortest }'\n            else\n                chroot $os_dir eselect profile list | grep stable | grep systemd |\n                    awk '{print length($2), $2}' | sort -n | head -1 | awk '{print $2}'\n            fi\n        )\n        echo \"Select profile: $profile\"\n        chroot $os_dir eselect profile set $profile\n\n        # 设置 license\n        cat <<EOF >>$os_dir/etc/portage/make.conf\nACCEPT_LICENSE=\"*\"\nEOF\n\n        cat <<EOF >>$os_dir/etc/portage/make.conf\nMAKEOPTS=\"-j$(get_build_threads 2048)\"\nEOF\n\n        # 设置 http repo + binpkg repo\n        # https://mirror.nju.edu.cn/gentoo/releases/amd64/autobuilds/current-stage3-amd64-systemd-mergedusr/stage3-amd64-systemd-mergedusr-20240317T170433Z.tar.xz\n        mirror_short=$(echo \"$img\" | sed 's,/releases/.*,,')\n        mirror_long=$(echo \"$img\" | sed 's,/autobuilds/.*,,')\n        profile_ver=$(chroot $os_dir eselect profile show | grep -Eo '/[0-9.]*/' | cut -d/ -f2)\n\n        if [ \"$(uname -m)\" = x86_64 ]; then\n            if chroot $os_dir ld.so --help | grep supported | grep -q x86-64-v3; then\n                binpkg_type=x86-64-v3\n            else\n                binpkg_type=x86-64\n            fi\n        else\n            binpkg_type=arm64\n        fi\n\n        cat <<EOF >>$os_dir/etc/portage/make.conf\nGENTOO_MIRRORS=\"$mirror_short\"\nFEATURES=\"getbinpkg\"\nEOF\n\n        cat <<EOF >$os_dir/etc/portage/binrepos.conf/gentoobinhost.conf\n[binhost]\npriority = 9999\nsync-uri = $mirror_long/binpackages/$profile_ver/$binpkg_type\nEOF\n\n        # 下载公钥\n\n        # getuto 会判断是否有 ${TERM} 且 ${TERM} 不是 dumb\n        # 符合条件则 source /lib/gentoo/functions.sh 导入 ebegin 等方法\n        # 不符合条件则自行创建 ebegin 等方法\n\n        # /lib/gentoo/functions.sh 会判断是否有 ${RC_OPENRC_PID}\n        # 有的话就不会导入 /functions/openrc.sh，也就不会导入 ebegin 方法\n\n        # 在 ssh 里运行 /trans.sh 时，没有 ${RC_OPENRC_PID}，${TERM} 是 xterm\n        # 因此 chroot $os_dir getuto 时不会报错\n\n        # 在 locald 里运行 /trans.sh 时，有 ${RC_OPENRC_PID}，${TERM} 是 linux\n        # 因此 chroot $os_dir getuto 时会报错说 ebegin: command not found\n\n        # 有两个解决方法\n        if true; then\n            TERM=dumb chroot $os_dir getuto\n        else\n            env -u RC_OPENRC_PID chroot $os_dir getuto\n        fi\n\n        set_locale\n\n        # 安装 git 会升级 glibc，此时 /etc/locale.gen 不能为空，否则会提示生成所有 locale\n        # Generating all locales; edit /etc/locale.gen to save time/space\n        chroot $os_dir emerge dev-vcs/git\n\n        # 设置 git repo\n        if is_in_china; then\n            git_uri=https://mirror.nju.edu.cn/git/gentoo-portage.git\n        else\n            # github 不支持 ipv6\n            is_any_ipv4_has_internet && git_uri=https://github.com/gentoo-mirror/gentoo.git ||\n                git_uri=https://anongit.gentoo.org/git/repo/gentoo.git\n        fi\n\n        mkdir -p $os_dir/etc/portage/repos.conf\n        cat <<EOF >$os_dir/etc/portage/repos.conf/gentoo.conf\n[gentoo]\nlocation = /var/db/repos/gentoo\nsync-type = git\nsync-uri = $git_uri\nEOF\n        rm -rf $os_dir/var/db/repos/gentoo\n        chroot $os_dir emerge --sync\n\n        if [ \"$(uname -m)\" = x86_64 ]; then\n            # https://packages.gentoo.org/packages/sys-block/io-scheduler-udev-rules\n            chroot $os_dir emerge sys-block/io-scheduler-udev-rules\n        fi\n\n        if is_efi; then\n            chroot $os_dir emerge sys-fs/dosfstools\n        fi\n\n        # firmware + microcode\n        if fw_pkgs=$(get_ucode_firmware_pkgs) && [ -n \"$fw_pkgs\" ]; then\n            chroot $os_dir emerge $fw_pkgs\n        fi\n\n        # 安装 grub + 内核\n        # TODO: 先判断是否有 binpkg，有的话不修改 GRUB_PLATFORMS\n        is_efi && grub_platforms=\"efi-64\" || grub_platforms=\"pc\"\n        echo GRUB_PLATFORMS=\\\"$grub_platforms\\\" >>$os_dir/etc/portage/make.conf\n        echo \"sys-kernel/installkernel dracut grub\" >$os_dir/etc/portage/package.use/installkernel\n        chroot $os_dir emerge sys-kernel/gentoo-kernel-bin\n    }\n\n    install_aosc() {\n        # 解压系统\n        apk add wget tar xz\n        wget \"$img\" -O- | tar xpJ --numeric-owner --xattrs-include='*.*' -C $os_dir\n        apk del wget tar xz\n\n        # 添加 swap\n        create_swap_if_ram_less_than 1024 $os_dir/swapfile\n\n        # 挂载伪文件系统\n        mount_pseudo_fs $os_dir\n\n        # 生成 initramfs\n        chroot $os_dir update-initramfs\n    }\n\n    os_dir=/os\n\n    # 挂载分区\n    mount_part_basic_layout /os /os/efi\n\n    # 安装系统\n    install_$distro\n\n    # 安装 arch 有 gpg-agent 进程驻留\n    pkill gpg-agent || true\n\n    # 初始化\n    if false; then\n        # preset-all 后多了很多服务，内存占用多了几十M\n        chroot $os_dir systemctl preset-all\n    fi\n\n    # 网络配置\n    case \"$network_app\" in\n    systemd-networkd)\n        chroot $os_dir systemctl enable systemd-networkd\n        chroot $os_dir systemctl enable systemd-resolved\n\n        apk add cloud-init\n        # 第二次运行会报错\n        useradd systemd-network || true\n        create_cloud_init_network_config net.cfg\n        cat -n net.cfg\n        # 正常应该是 -D gentoo，但 alpine 的 cloud-init 包缺少 gentoo 配置\n        cloud-init devel net-convert -p net.cfg -k yaml -d out -D alpine -O networkd\n\n        # 注意名字是 10-cloud-init-eth*.network，fix-eth-name.sh 会此文件名查找配置文件\n        cp out/etc/systemd/network/10-cloud-init-eth*.network $os_dir/etc/systemd/network/\n\n        # 删除网卡名匹配\n        sed -i '/^Name=/d' $os_dir/etc/systemd/network/10-cloud-init-eth*.network\n\n        # 删除 Generated by cloud-init. Changes will be lost.\n        # 并删除头部的空行\n        sed -i '/^# Generated by cloud-init/d' $os_dir/etc/systemd/network/10-cloud-init-eth*.network\n        del_head_empty_lines_inplace $os_dir/etc/systemd/network/10-cloud-init-eth*.network\n\n        # 清理\n        rm -rf net.cfg out\n        apk del cloud-init\n\n        # 显示网络配置\n        cat -n $os_dir/etc/systemd/network/10-cloud-init-eth*.network\n        ;;\n    network-manager)\n        chroot $os_dir systemctl enable NetworkManager\n\n        # 可以直接用 alpine 的 cloud-init 生成 Network Manager 配置\n        create_cloud_init_network_config /net.cfg\n        create_network_manager_config /net.cfg \"$os_dir\"\n        rm /net.cfg\n        ;;\n    esac\n\n    # arch gentoo 网络配置是用 alpine cloud-init 生成的\n    # cloud-init 版本够新，因此无需修复 onlink 网关\n\n    basic_init $os_dir\n\n    # ntp 用 systemd 自带的\n    # TODO: vm agent + 随机数生成器\n\n    # grub\n    if is_efi; then\n        # arch gentoo 推荐 efi 挂载在 /efi\n        chroot $os_dir grub-install --efi-directory=/efi\n        chroot $os_dir grub-install --efi-directory=/efi --removable\n    else\n        chroot $os_dir grub-install /dev/$xda\n    fi\n\n    # cmdline + 生成 grub.cfg\n    if [ -d $os_dir/etc/default/grub.d ]; then\n        file=$os_dir/etc/default/grub.d/tty.cfg\n    else\n        file=$os_dir/etc/default/grub\n    fi\n    ttys_cmdline=$(get_ttys console=)\n    echo GRUB_CMDLINE_LINUX=\\\"\\$GRUB_CMDLINE_LINUX $ttys_cmdline\\\" >>$file\n    chroot $os_dir grub-mkconfig -o /boot/grub/grub.cfg\n\n    # fstab\n    # fstab 可不写 efi 条目， systemd automount 会自动挂载\n    # fstab 头部有使用说明，因此用 >>\n    apk add arch-install-scripts\n    genfstab -U $os_dir | sed '/swap/d' >>$os_dir/etc/fstab\n    apk del arch-install-scripts\n\n    # 删除 resolv.conf，不然 systemd-resolved 无法创建软链接\n    rm_resolv_conf $os_dir\n\n    # 删除 swap\n    swapoff -a\n    rm -rf $os_dir/swapfile\n}\n\nget_http_file_size() {\n    url=$1\n\n    # 网址重定向可能得到多个 Content-Length, 选最后一个\n    wget --spider -S \"$url\" 2>&1 | grep 'Content-Length:' |\n        tail -1 | awk '{print $2}' | grep .\n}\n\nget_url_hash() {\n    url=$1\n\n    echo \"$url\" | md5sum | awk '{print $1}'\n}\n\naria2c() {\n    if ! is_have_cmd aria2c; then\n        apk add aria2\n    fi\n\n    # stdbuf 在 coreutils 包里面\n    if ! is_have_cmd stdbuf; then\n        apk add coreutils\n    fi\n\n    # 显示 url\n    show_url_in_args \"$@\" >&2\n\n    # 下载 tracker\n    # 在 sub shell 里面无法保存变量，因此写入到文件\n    if echo \"$@\" | grep -Eq 'magnet:|\\.torrent' && ! [ -f \"/tmp/trackers\" ]; then\n        # 独自一行下载，不然下载失败不会报错\n        # 里面有空行\n        # txt=$(wget -O- https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt | grep .)\n        # txt=$(wget -O- https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt | grep .)\n        txt=$(wget -O- https://cf.trackerslist.com/best.txt | grep .)\n        # sed 删除最后一个逗号\n        echo \"$txt\" | newline_to_comma | sed 's/,$//' >/tmp/trackers\n    fi\n\n    # --dht-entry-point=router.bittorrent.com:6881 \\\n    # --dht-entry-point=dht.transmissionbt.com:6881 \\\n    # --dht-entry-point=router.utorrent.com:6881 \\\n    retry 5 5 stdbuf -oL -eL aria2c \\\n        -x4 \\\n        --seed-time=0 \\\n        --allow-overwrite=true \\\n        --summary-interval=0 \\\n        --max-tries 1 \\\n        --bt-tracker=\"$([ -f \"/tmp/trackers\" ] && cat /tmp/trackers)\" \\\n        \"$@\"\n}\n\ndownload_torrent_by_magnet() {\n    url=$1\n    dst=$2\n\n    url_hash=$(get_url_hash \"$url\")\n\n    mkdir -p /tmp/bt/$url_hash\n\n    # 不支持 -o bt.torrent 指定文件名\n    aria2c \"$url\" \\\n        --bt-metadata-only=true \\\n        --bt-save-metadata=true \\\n        -d /tmp/bt/$url_hash\n\n    mv /tmp/bt/$url_hash/*.torrent \"$dst\"\n    rm -rf /tmp/bt/$url_hash\n}\n\nget_torrent_path_by_magnet() {\n    echo \"/tmp/bt/$(get_url_hash \"$1\").torrent\"\n}\n\nget_bt_file_size() {\n    url=$1\n\n    torrent=\"$(get_torrent_path_by_magnet $url)\"\n    download_torrent_by_magnet \"$url\" \"$torrent\" >&2\n\n    # 列出第一个文件的大小\n    # idx|path/length\n    # ===+===========================================================================\n    #   1|./zh-cn_windows_11_consumer_editions_version_24h2_updated_jan_2025_x64_dvd_7a8e5a29.iso\n    #    |6.1GiB (6,557,558,784)\n\n    aria2c --show-files=true \"$torrent\" |\n        grep -F -A1 '  1|./' | tail -1 | grep -o '(.*)' | sed -E 's/[(),]//g' | grep .\n}\n\nget_link_file_size() {\n    if is_magnet_link \"$1\" >&2; then\n        get_bt_file_size \"$1\"\n    else\n        get_http_file_size \"$1\"\n    fi\n}\n\npipe_extract() {\n    # alpine busybox 自带 gzip，但官方版也许性能更好\n    case \"$img_type_warp\" in\n    xz | gzip | zstd)\n        apk add $img_type_warp\n        \"$img_type_warp\" -dc\n        ;;\n    tar)\n        apk add tar\n        tar x -O\n        ;;\n    tar.*)\n        type=$(echo \"$img_type_warp\" | cut -d. -f2)\n        apk add tar \"$type\"\n        tar x \"--$type\" -O\n        ;;\n    '') cat ;;\n    *) error_and_exit \"Not supported img_type_warp: $img_type_warp\" ;;\n    esac\n}\n\ndd_raw_with_extract() {\n    info \"dd raw\"\n\n    # 用官方 wget，一来带进度条，二来自带重试功能\n    apk add wget\n\n    if ! wget $img -O- | pipe_extract >/dev/$xda 2>/tmp/dd_stderr; then\n        # vhd 文件结尾有 512 字节额外信息，可以忽略\n        if grep -iq 'No space' /tmp/dd_stderr; then\n            apk add parted\n            disk_size=$(get_disk_size /dev/$xda)\n            disk_end=$((disk_size - 1))\n\n            # 如果报错，那大概是因为镜像比硬盘大\n            if last_part_end=$(parted -sf /dev/$xda 'unit b print' ---pretend-input-tty |\n                del_empty_lines | tail -1 | awk '{print $3}' | sed 's/B//' | grep .); then\n\n                echo \"Last part end: $last_part_end\"\n                echo \"Disk end:      $disk_end\"\n\n                if [ \"$last_part_end\" -le \"$disk_end\" ]; then\n                    echo \"Safely ignore no space error.\"\n                    return\n                fi\n            fi\n        fi\n        error_and_exit \"$(cat /tmp/dd_stderr)\"\n    fi\n}\n\nget_disk_sector_count() {\n    # cat /proc/partitions\n    blockdev --getsz \"$1\"\n}\n\nget_disk_size() {\n    blockdev --getsize64 \"$1\"\n}\n\nget_disk_logic_sector_size() {\n    blockdev --getss \"$1\"\n}\n\nis_4kn() {\n    [ \"$(blockdev --getss \"/dev/$xda\")\" = 4096 ]\n}\n\nis_xda_gt_2t() {\n    disk_size=$(get_disk_size /dev/$xda)\n    disk_2t=$((2 * 1024 * 1024 * 1024 * 1024))\n    [ \"$disk_size\" -gt \"$disk_2t\" ]\n}\n\ncreate_part() {\n    # 除了 dd 都会用到\n    info \"Create Part\"\n\n    # 分区工具\n    apk add parted e2fsprogs\n    if is_efi; then\n        apk add dosfstools\n    fi\n\n    # 清除分区签名\n    # TODO: 先检测iso链接/各种链接\n    # wipefs -a /dev/$xda\n\n    # xda*1 星号用于 nvme0n1p1 的字母 p\n    # shellcheck disable=SC2154\n    if [ \"$distro\" = windows ]; then\n        if ! size_bytes=$(get_link_file_size \"$iso\"); then\n            # 默认值，目前最大的 iso 小于 8g\n            size_bytes=$((8 * 1024 * 1024 * 1024))\n        fi\n\n        # 按iso容量计算分区大小\n        # 200m 用于驱动/文件系统自身占用 + pagefile\n        # 理论上 installer 分区可以删除 boot.wim，这样就不用额外添加 200m，但是\n        # 1. vista/2008 不能删除 boot.wim\n        # 2. 下载镜像前不知道是 vista/2008，因为 --image-name 可以随便输入\n        # 因此还是要额外添加 200m\n        # 注意这里单位要用 MiB，因为后面的 border 要以 MiB 计算\n        part_size=\"$((size_bytes / 1024 / 1024 + 200))MiB\"\n\n        apk add ntfs-3g-progs\n        # 虽然ntfs3不需要fuse，但wimmount需要，所以还是要保留\n        modprobe fuse ntfs3\n        if is_efi; then\n            # efi\n            parted /dev/$xda -s -- \\\n                mklabel gpt \\\n                mkpart '\" \"' fat32 1MiB 1025MiB \\\n                mkpart '\" \"' fat32 1025MiB 1041MiB \\\n                mkpart '\" \"' ntfs 1041MiB -${part_size} \\\n                mkpart '\" \"' ntfs -${part_size} 100% \\\n                set 1 boot on \\\n                set 2 msftres on \\\n                set 3 msftdata on\n            update_part\n\n            mkfs.fat -n efi /dev/$xda*1                           #1 efi\n            dd if=/dev/zero of=\"$(ls /dev/$xda*2)\" bs=1M count=16 #2 msr\n            mkfs.ntfs -f -F -L os /dev/$xda*3                     #3 os\n            mkfs.ntfs -f -F -L installer /dev/$xda*4              #4 installer\n        else\n            # bios + mbr 启动盘最大可用 2t\n            if is_xda_gt_2t; then\n                border=$((2 * 1024 * 1024 - ${part_size%MiB}))MiB\n                max_usable_size=2TiB\n            else\n                border=-${part_size}\n                max_usable_size=100%\n            fi\n            parted /dev/$xda -s -- \\\n                mklabel msdos \\\n                mkpart primary ntfs 1MiB ${border} \\\n                mkpart primary ntfs ${border} ${max_usable_size} \\\n                set 1 boot on\n            update_part\n\n            mkfs.ntfs -f -F -L os /dev/$xda*1        #1 os\n            mkfs.ntfs -f -F -L installer /dev/$xda*2 #2 installer\n        fi\n    elif [ \"$distro\" = fnos ]; then\n        # 先用 100% 分区安装后再缩小没意义，因为小硬盘用 100% 还是装不了\n        # 因此直接用用户输入的分区大小\n\n        # 1. 官方安装器对系统盘大小的定义包含引导分区大小\n        # 2. 官方 efi 用的是 1MiB-100M，但我们用 1MiB-101MiB\n\n        # 预期的系统分区大小，包括引导的 1M + 100M 的引导分区\n        expect_m=$((${fnos_part_size%[Gg]} * 1024))\n\n        sector_size=$(get_disk_logic_sector_size /dev/$xda)\n        total_sector_count=$(get_disk_sector_count /dev/$xda)\n\n        # 截止最后一个分区的总扇区数（也就是总硬盘扇区数 - 备份分区表扇区数 - 备份 GPT Header）\n        if ! is_efi && ! is_xda_gt_2t; then\n            # mbr\n            total_sector_count_except_backup_gpt=$total_sector_count\n        elif is_4kn; then\n            total_sector_count_except_backup_gpt=$((total_sector_count - 4 - 1))\n        else\n            total_sector_count_except_backup_gpt=$((total_sector_count - 32 - 1))\n        fi\n\n        # 向下取整 MiB\n        # gpt 最后 33 (512n/512e) 或 5 (4Kn) 个扇区是备份分区表，不可用\n        # parted 结束位置填 100% 时也会忽略最后不足 1MiB 的部分，我们模仿它\n        max_can_use_m=$((total_sector_count_except_backup_gpt * sector_size / 1024 / 1024))\n\n        echo \"expect_m: $expect_m\"\n        echo \"max_can_use_m: $max_can_use_m\"\n\n        # 20G 的硬盘，即使用 msdos 分区表，parted 也不接受 part end 为 20480MiB，因此要用 100%\n        # The location 20480MiB is outside of the device /dev/vda.\n        # 但是 100% 分区后 end 就是 20480MiB\n\n        os_part_end=${expect_m}MiB\n        if [ \"$expect_m\" -ge \"$max_can_use_m\" ]; then\n            echo \"Expect size is equal/greater than max size. Setting to 100%\"\n            os_part_end=100%\n        fi\n\n        # 需关闭这几个特性，否则 grub 无法识别\n        ext4_opts=\"-O ^metadata_csum_seed,^orphan_file\"\n        if is_efi; then\n            parted /dev/$xda -s -- \\\n                mklabel gpt \\\n                mkpart BOOT fat32 1MiB 101MiB \\\n                mkpart SYSTEM ext4 101MiB $os_part_end \\\n                set 1 esp on\n            update_part\n\n            mkfs.fat /dev/$xda*1                #1 efi\n            mkfs.ext4 -F $ext4_opts /dev/$xda*2 #2 os + installer\n        elif is_xda_gt_2t; then\n            # bios > 2t\n            # 官方安装器是 mkpart BOOT 1M 100M，无论 esp 或者 bios_grub 都用这个分区和大小\n            parted /dev/$xda -s -- \\\n                mklabel gpt \\\n                mkpart BOOT ext4 1MiB 101MiB \\\n                mkpart SYSTEM ext4 101MiB $os_part_end \\\n                set 1 bios_grub on\n            update_part\n\n            echo                                #1 bios_boot\n            mkfs.ext4 -F $ext4_opts /dev/$xda*2 #2 os + installer\n        else\n            # bios\n            parted /dev/$xda -s -- \\\n                mklabel msdos \\\n                mkpart primary 1MiB 101MiB \\\n                mkpart primary 101MiB $os_part_end \\\n                set 2 boot on\n            update_part\n\n            echo                                #1 官方安装有这个分区\n            mkfs.ext4 -F $ext4_opts /dev/$xda*2 #2 os + installer\n        fi\n    elif is_use_cloud_image; then\n        installer_part_size=\"$(get_cloud_image_part_size)\"\n        # 这几个系统不使用dd，而是复制文件\n        if [ \"$distro\" = centos ] || [ \"$distro\" = almalinux ] || [ \"$distro\" = rocky ] ||\n            [ \"$distro\" = oracle ] || [ \"$distro\" = redhat ] ||\n            [ \"$distro\" = anolis ] || [ \"$distro\" = opencloudos ] || [ \"$distro\" = openeuler ] ||\n            [ \"$distro\" = ubuntu ]; then\n            # 这里的 fs 没有用，最终使用目标系统的格式化工具\n            fs=ext4\n            if is_efi; then\n                parted /dev/$xda -s -- \\\n                    mklabel gpt \\\n                    mkpart '\" \"' fat32 1MiB 101MiB \\\n                    mkpart '\" \"' $fs 101MiB -$installer_part_size \\\n                    mkpart '\" \"' ext4 -$installer_part_size 100% \\\n                    set 1 esp on\n                update_part\n\n                mkfs.fat -n efi /dev/$xda*1           #1 efi\n                echo                                  #2 os 用目标系统的格式化工具\n                mkfs.ext4 -F -L installer /dev/$xda*3 #3 installer\n            else\n                parted /dev/$xda -s -- \\\n                    mklabel gpt \\\n                    mkpart '\" \"' ext4 1MiB 2MiB \\\n                    mkpart '\" \"' $fs 2MiB -$installer_part_size \\\n                    mkpart '\" \"' ext4 -$installer_part_size 100% \\\n                    set 1 bios_grub on\n                update_part\n\n                echo                                  #1 bios_boot\n                echo                                  #2 os 用目标系统的格式化工具\n                mkfs.ext4 -F -L installer /dev/$xda*3 #3 installer\n            fi\n        else\n            # 使用 dd qcow2\n            # fedora debian opensuse arch gentoo\n            parted /dev/$xda -s -- \\\n                mklabel gpt \\\n                mkpart '\" \"' ext4 1MiB -$installer_part_size \\\n                mkpart '\" \"' ext4 -$installer_part_size 100%\n            update_part\n\n            mkfs.ext4 -F -L os /dev/$xda*1        #1 os\n            mkfs.ext4 -F -L installer /dev/$xda*2 #2 installer\n        fi\n    elif [ \"$distro\" = alpine ] || [ \"$distro\" = arch ] || [ \"$distro\" = gentoo ] ||\n        [ \"$distro\" = nixos ] || [ \"$distro\" = aosc ]; then\n        # alpine 本身关闭了 64bit ext4\n        # https://gitlab.alpinelinux.org/alpine/alpine-conf/-/blob/3.18.1/setup-disk.in?ref_type=tags#L908\n        # 而且 alpine 的 extlinux 不兼容 64bit ext4\n        [ \"$distro\" = alpine ] && ext4_opts=\"-O ^64bit\" || ext4_opts=\n        if is_efi; then\n            # efi\n            parted /dev/$xda -s -- \\\n                mklabel gpt \\\n                mkpart '\" \"' fat32 1MiB 101MiB \\\n                mkpart '\" \"' ext4 101MiB 100% \\\n                set 1 boot on\n            update_part\n\n            mkfs.fat /dev/$xda*1                #1 efi\n            mkfs.ext4 -F $ext4_opts /dev/$xda*2 #2 os\n        elif is_xda_gt_2t; then\n            # bios > 2t\n            parted /dev/$xda -s -- \\\n                mklabel gpt \\\n                mkpart '\" \"' ext4 1MiB 2MiB \\\n                mkpart '\" \"' ext4 2MiB 100% \\\n                set 1 bios_grub on\n            update_part\n\n            echo                                #1 bios_boot\n            mkfs.ext4 -F $ext4_opts /dev/$xda*2 #2 os\n        else\n            # bios\n            parted /dev/$xda -s -- \\\n                mklabel msdos \\\n                mkpart primary ext4 1MiB 100% \\\n                set 1 boot on\n            update_part\n\n            mkfs.ext4 -F $ext4_opts /dev/$xda*1 #1 os\n        fi\n    else\n        # 安装红帽系或ubuntu\n        # 对于红帽系是临时分区表，安装时除了 installer 分区，其他分区会重建为默认的大小\n        # 对于ubuntu是最终分区表，因为 ubuntu 的安装器不能调整个别分区，只能重建整个分区表\n        # installer 2g分区用fat格式刚好塞得下ubuntu-22.04.3 iso，而ext4塞不下或者需要改参数\n        if [ \"$distro\" = ubuntu ]; then\n            if ! size_bytes=$(get_http_file_size \"$iso\"); then\n                # 默认值，假设 iso 3g\n                size_bytes=$((3 * 1024 * 1024 * 1024))\n            fi\n            installer_part_size=\"$(get_part_size_mb_for_file_size_b $size_bytes)MiB\"\n        else\n            # redhat\n            installer_part_size=2GiB\n        fi\n\n        # centos 7 无法加载alpine格式化的ext4\n        # 要关闭这个属性\n        ext4_opts=\"-O ^metadata_csum\"\n        apk add dosfstools\n\n        if is_efi; then\n            # efi\n            parted /dev/$xda -s -- \\\n                mklabel gpt \\\n                mkpart '\" \"' fat32 1MiB 1025MiB \\\n                mkpart '\" \"' ext4 1025MiB -$installer_part_size \\\n                mkpart '\" \"' ext4 -$installer_part_size 100% \\\n                set 1 boot on\n            update_part\n\n            mkfs.fat -n efi /dev/$xda*1                      #1 efi\n            mkfs.ext4 -F -L os /dev/$xda*2                   #2 os\n            mkfs.ext4 -F -L installer $ext4_opts /dev/$xda*3 #2 installer\n        elif is_xda_gt_2t; then\n            # bios > 2t\n            parted /dev/$xda -s -- \\\n                mklabel gpt \\\n                mkpart '\" \"' ext4 1MiB 2MiB \\\n                mkpart '\" \"' ext4 2MiB -$installer_part_size \\\n                mkpart '\" \"' ext4 -$installer_part_size 100% \\\n                set 1 bios_grub on\n            update_part\n\n            echo                                             #1 bios_boot\n            mkfs.ext4 -F -L os /dev/$xda*2                   #2 os\n            mkfs.ext4 -F -L installer $ext4_opts /dev/$xda*3 #3 installer\n        else\n            # bios\n            parted /dev/$xda -s -- \\\n                mklabel msdos \\\n                mkpart primary ext4 1MiB -$installer_part_size \\\n                mkpart primary ext4 -$installer_part_size 100% \\\n                set 1 boot on\n            update_part\n\n            mkfs.ext4 -F -L os /dev/$xda*1                   #1 os\n            mkfs.ext4 -F -L installer $ext4_opts /dev/$xda*2 #2 installer\n        fi\n        update_part\n    fi\n\n    update_part\n\n    # alpine 删除分区工具，防止 256M 小机爆内存\n    # setup-disk /dev/sda 会保留格式化工具，我们也保留\n    if [ \"$distro\" = alpine ]; then\n        apk del parted\n    fi\n}\n\numount_pseudo_fs() {\n    os_dir=$(realpath \"$1\")\n\n    dirs=\"/proc /sys /dev /run\"\n    regex=$(echo \"$dirs\" | sed 's, ,|,g')\n    if mounts=$(mount | grep -Ew \"on $os_dir($regex)\" | awk '{print $3}' | tac); then\n        for mount in $mounts; do\n            echo \"umount $mount\"\n            umount $mount\n        done\n    fi\n}\n\nmount_pseudo_fs() {\n    os_dir=$1\n\n    mkdir -p $os_dir/proc/ $os_dir/sys/ $os_dir/dev/ $os_dir/run/\n\n    # https://wiki.archlinux.org/title/Chroot#Using_chroot\n    mount -t proc /proc $os_dir/proc/\n    mount -t sysfs /sys $os_dir/sys/\n    mount --rbind /dev $os_dir/dev/\n    mount --rbind /run $os_dir/run/\n    if is_efi; then\n        mount --rbind /sys/firmware/efi/efivars $os_dir/sys/firmware/efi/efivars/\n    fi\n}\n\ncreate_cloud_init_network_config() {\n    ci_file=$1\n    recognize_static6=${2:-true}\n    recognize_ipv6_types=${3:-true}\n\n    info \"Create Cloud-init network config\"\n\n    # 防止文件未创建\n    mkdir -p \"$(dirname \"$ci_file\")\"\n    touch \"$ci_file\"\n\n    apk add yq-go\n\n    need_set_dns4=false\n    need_set_dns6=false\n\n    config_id=0\n    for ethx in $(get_eths); do\n        get_netconf_to mac_addr\n\n        # shellcheck disable=SC2154\n        yq -i \".network.version=1 |\n           .network.config[$config_id].type=\\\"physical\\\" |\n           .network.config[$config_id].name=\\\"$ethx\\\" |\n           .network.config[$config_id].mac_address=(\\\"$mac_addr\\\" | . style=\\\"single\\\")\n           \" $ci_file\n\n        subnet_id=0\n\n        # ipv4\n        if is_dhcpv4; then\n            yq -i \".network.config[$config_id].subnets[$subnet_id] = {\\\"type\\\": \\\"dhcp4\\\"}\" $ci_file\n            subnet_id=$((subnet_id + 1))\n        elif is_staticv4; then\n            need_set_dns4=true\n            get_netconf_to ipv4_addr\n            get_netconf_to ipv4_gateway\n            yq -i \".network.config[$config_id].subnets[$subnet_id] = {\n                    \\\"type\\\": \\\"static\\\",\n                    \\\"address\\\": \\\"$ipv4_addr\\\",\n                    \\\"gateway\\\": \\\"$ipv4_gateway\\\" }\n                    \" $ci_file\n\n            # 旧版 cloud-init 有 bug\n            # 有的版本会只从第一种配置中读取 dns，有的从第二种读取\n            # 因此写两种配置\n            # https://github.com/canonical/cloud-init/commit/1b8030e0c7fd6fbff7e38ad1e3e6266ae50c83a5\n            for cur in $(get_current_dns 4); do\n                yq -i \".network.config[$config_id].subnets[$subnet_id].dns_nameservers += [\\\"$cur\\\"]\" $ci_file\n            done\n            subnet_id=$((subnet_id + 1))\n        fi\n\n        # ipv6\n        # slaac:  ipv6_slaac\n        # └─enable_other_flag: ipv6_dhcpv6-stateless\n        # dhcpv6: ipv6_dhcpv6-stateful\n\n        # ipv6\n        if is_slaac; then\n            if $recognize_ipv6_types; then\n                if is_enable_other_flag; then\n                    type=ipv6_dhcpv6-stateless\n                else\n                    type=ipv6_slaac\n                fi\n            else\n                type=dhcp6\n            fi\n            yq -i \".network.config[$config_id].subnets[$subnet_id] = {\\\"type\\\": \\\"$type\\\"}\" $ci_file\n\n        elif is_dhcpv6; then\n            if $recognize_ipv6_types; then\n                type=ipv6_dhcpv6-stateful\n            else\n                type=dhcp6\n            fi\n            yq -i \".network.config[$config_id].subnets[$subnet_id] = {\\\"type\\\": \\\"$type\\\"}\" $ci_file\n\n        elif is_staticv6; then\n            get_netconf_to ipv6_addr\n            get_netconf_to ipv6_gateway\n            if $recognize_static6; then\n                type_ipv6_static=static6\n            else\n                type_ipv6_static=static\n            fi\n            yq -i \".network.config[$config_id].subnets[$subnet_id] = {\n                    \\\"type\\\": \\\"$type_ipv6_static\\\",\n                    \\\"address\\\": \\\"$ipv6_addr\\\",\n                    \\\"gateway\\\": \\\"$ipv6_gateway\\\" }\n                    \" $ci_file\n        fi\n        # 无法设置 autoconf = false ?\n        if should_disable_accept_ra; then\n            yq -i \".network.config[$config_id].accept-ra = false\" $ci_file\n        fi\n\n        # 有 ipv6 但需设置 dns 的情况\n        if is_need_manual_set_dnsv6; then\n            need_set_dns6=true\n            for cur in $(get_current_dns 6); do\n                yq -i \".network.config[$config_id].subnets[$subnet_id].dns_nameservers += [\\\"$cur\\\"]\" $ci_file\n            done\n        fi\n\n        config_id=$((config_id + 1))\n    done\n\n    if $need_set_dns4 || $need_set_dns6; then\n        yq -i \".network.config[$config_id].type=\\\"nameserver\\\"\" $ci_file\n        if $need_set_dns4; then\n            for cur in $(get_current_dns 4); do\n                yq -i \".network.config[$config_id].address += [\\\"$cur\\\"]\" $ci_file\n            done\n        fi\n        if $need_set_dns6; then\n            for cur in $(get_current_dns 6); do\n                yq -i \".network.config[$config_id].address += [\\\"$cur\\\"]\" $ci_file\n            done\n        fi\n        # 如果 network.config[$config_id] 没有 address，则删除，避免低版本 cloud-init 报错\n        yq -i \"del(.network.config[$config_id] | select(has(\\\"address\\\") | not))\" $ci_file\n    fi\n\n    apk del yq-go\n\n    # 查看文件\n    info \"Cloud-init network config\"\n    cat -n $ci_file >&2\n}\n\n# 实测没用，生成的 machine-id 是固定的\n# 而且 lightsail centos 9 模板 machine-id 也是相同的，显然相同 id 不是个问题\nclear_machine_id() {\n    os_dir=$1\n\n    # https://www.freedesktop.org/software/systemd/man/latest/machine-id.html\n    # gentoo 不会自动创建该文件\n    echo uninitialized >$os_dir/etc/machine-id\n\n    # https://build.opensuse.org/projects/Virtualization:Appliances:Images:openSUSE-Leap-15.5/packages/kiwi-templates-Minimal/files/config.sh?expand=1\n    rm -f $os_dir/var/lib/systemd/random-seed\n}\n\n# 注意 anolis 7 有这个文件，可能干扰我们的配置?\n# /etc/cloud/cloud.cfg.d/aliyun_cloud.cfg -> /sys/firmware/qemu_fw_cfg/by_name/etc/cloud-init/vendor-data/raw\ndownload_cloud_init_config() {\n    os_dir=$1\n    recognize_static6=$2\n    recognize_ipv6_types=$3\n\n    ci_file=$os_dir/etc/cloud/cloud.cfg.d/99_fallback.cfg\n    download $confhome/cloud-init.yaml $ci_file\n    # 删除注释行，除了第一行\n    sed -i '1!{/^[[:space:]]*#/d}' $ci_file\n\n    # 修改密码\n    # 不能用 sed 替换，因为含有特殊字符\n    content=$(cat $ci_file)\n    echo \"${content//@PASSWORD@/$(get_password_linux_sha512)}\" >$ci_file\n\n    # 修改 ssh 端口\n    if is_need_change_ssh_port; then\n        sed -i \"s/@SSH_PORT@/$ssh_port/g\" $ci_file\n    else\n        sed -i \"/@SSH_PORT@/d\" $ci_file\n    fi\n\n    # swapfile\n    # 如果分区表中已经有swapfile就跳过，例如arch\n    if ! grep -w swap $os_dir/etc/fstab; then\n        cat <<EOF >>$ci_file\nswap:\n  filename: /swapfile\n  size: auto\nEOF\n    fi\n\n    create_cloud_init_network_config \"$ci_file\" \"$recognize_static6\" \"$recognize_ipv6_types\"\n}\n\nget_image_state() {\n    local os_dir=$1\n    local image_state=\n\n    # 如果 dd 镜像精简了 State.ini，则从注册表获取\n    if state_ini=$(find_file_ignore_case $os_dir/Windows/Setup/State/State.ini); then\n        image_state=$(grep -i '^ImageState=' $state_ini | cut -d= -f2 | tr -d '\\r')\n    fi\n    if [ -z \"$image_state\" ]; then\n        apk add hivex\n        hive=$(find_file_ignore_case $os_dir/Windows/System32/config/SOFTWARE)\n        image_state=$(hivexget $hive '\\Microsoft\\Windows\\CurrentVersion\\Setup\\State' ImageState)\n        apk del hivex\n    fi\n\n    if [ -n \"$image_state\" ]; then\n        echo \"$image_state\"\n    else\n        error_and_exit \"Cannot get ImageState.\"\n    fi\n}\n\nmodify_windows() {\n    os_dir=$1\n    info \"Modify Windows\"\n\n    # https://learn.microsoft.com/windows-hardware/manufacture/desktop/windows-setup-states\n    # https://learn.microsoft.com/troubleshoot/azure/virtual-machines/reset-local-password-without-agent\n    # https://learn.microsoft.com/windows-hardware/manufacture/desktop/add-a-custom-script-to-windows-setup\n\n    # 判断用 SetupComplete 还是组策略\n    image_state=$(get_image_state \"$os_dir\")\n    echo \"ImageState: $image_state\"\n\n    if [ \"$image_state\" = IMAGE_STATE_COMPLETE ]; then\n        use_gpo=true\n    else\n        use_gpo=false\n    fi\n\n    # bat 列表\n    bats=\n\n    # 1. rdp 端口\n    if is_need_change_rdp_port; then\n        create_win_change_rdp_port_script $os_dir/windows-change-rdp-port.bat \"$rdp_port\"\n        bats=\"$bats windows-change-rdp-port.bat\"\n    fi\n\n    # 2. 允许 ping\n    if is_allow_ping; then\n        download $confhome/windows-allow-ping.bat $os_dir/windows-allow-ping.bat\n        bats=\"$bats windows-allow-ping.bat\"\n    fi\n\n    # 3. 合并分区\n    # 可能 unattend.xml 已经设置了ExtendOSPartition，不过运行resize没副作用\n    download $confhome/windows-resize.bat $os_dir/windows-resize.bat\n    bats=\"$bats windows-resize.bat\"\n\n    # 4. 网络设置\n    for ethx in $(get_eths); do\n        create_win_set_netconf_script $os_dir/windows-set-netconf-$ethx.bat\n        bats=\"$bats windows-set-netconf-$ethx.bat\"\n    done\n\n    # 5 frp\n    if ls /configs/frpc.* >/dev/null 2>&1; then\n        if [ \"$(get_windows_arch_from_windows_drive \"$os_dir\" | to_lower)\" = x86 ]; then\n            os_bit=32\n        else\n            os_bit=64\n        fi\n        mkdir -p \"$os_dir/frpc/\"\n        url=$(get_frpc_url windows \"$nt_ver\" \"$os_bit\")\n        download \"$url\" $os_dir/frpc/frpc.zip\n        # -j 去除文件夹\n        # -C 筛选文件时不区分大小写，但 busybox zip 不支持\n        unzip -o -j \"$os_dir/frpc/frpc.zip\" '*/frpc.exe' -d \"$os_dir/frpc/\"\n        rm -f \"$os_dir/frpc/frpc.zip\"\n        cp -f /configs/frpc.* \"$os_dir/frpc/\"\n        download \"$confhome/windows-frpc.xml\" \"$os_dir/frpc/frpc.xml\"\n        download \"$confhome/windows-frpc.bat\" \"$os_dir/frpc/frpc.bat\"\n        bats=\"$bats frpc\\frpc.bat\"\n    fi\n\n    if $use_gpo; then\n        # 使用组策略\n        scripts_ini=$(get_path_in_correct_case $os_dir/Windows/System32/GroupPolicy/Machine/Scripts/scripts.ini)\n        mkdir -p \"$(dirname $scripts_ini)\"\n        gpt_ini=$(get_path_in_correct_case $os_dir/Windows/System32/GroupPolicy/gpt.ini)\n\n        # 备份 ini\n        for file in $gpt_ini $scripts_ini; do\n            if [ -f $file ]; then\n                cp $file $file.orig\n            fi\n        done\n\n        # gpt.ini\n        cat >$gpt_ini <<EOF\n[General]\ngPCFunctionalityVersion=2\ngPCMachineExtensionNames=[{42B5FAAE-6536-11D2-AE5A-0000F87571E3}{40B6664F-4972-11D1-A7CA-0000F87571E3}]\nVersion=1\nEOF\n        unix2dos $gpt_ini\n\n        # scripts.ini\n        if ! [ -e $scripts_ini ]; then\n            touch $scripts_ini\n        fi\n\n        if ! grep -F '[Startup]' $scripts_ini; then\n            echo '[Startup]' >>$scripts_ini\n        fi\n\n        # 注意没用 pipefail 的话，错误码取自最后一个管道\n        if num=$(grep -Eo '^[0-9]+' $scripts_ini | sort -n | tail -1 | grep .); then\n            num=$((num + 1))\n        else\n            num=0\n        fi\n\n        bats=\"$bats windows-del-gpo.bat\"\n        for bat in $bats; do\n            echo \"${num}CmdLine=%SystemDrive%\\\\$bat\" >>$scripts_ini\n            echo \"${num}Parameters=\" >>$scripts_ini\n            num=$((num + 1))\n        done\n        cat $scripts_ini\n        unix2dos $scripts_ini\n\n        # windows-del-gpo.bat\n        download $confhome/windows-del-gpo.bat $os_dir/windows-del-gpo.bat\n    else\n        # 使用 SetupComplete\n        setup_complete=$(get_path_in_correct_case $os_dir/Windows/Setup/Scripts/SetupComplete.cmd)\n        mkdir -p \"$(dirname $setup_complete)\"\n\n        # 添加到 C:\\Setup\\Scripts\\SetupComplete.cmd 最前面\n        # call 防止子 bat 删除自身后中断主脚本\n        setup_complete_mod=$(mktemp)\n        for bat in $bats; do\n            echo \"if exist %SystemDrive%\\\\$bat (call %SystemDrive%\\\\$bat)\" >>$setup_complete_mod\n        done\n\n        # 复制原来的内容\n        if [ -f $setup_complete ]; then\n            cat $setup_complete >>$setup_complete_mod\n        fi\n\n        unix2dos $setup_complete_mod\n\n        # cat 可以保留权限\n        cat $setup_complete_mod >$setup_complete\n\n        # 查看最终内容\n        cat -n $setup_complete\n    fi\n}\n\nget_axx64() {\n    case \"$(uname -m)\" in\n    x86_64) echo amd64 ;;\n    aarch64) echo arm64 ;;\n    esac\n}\n\nis_file_or_link() {\n    # -e / -f 坏软连接，返回 false\n    # -L 坏软连接，返回 true\n    [ -f $1 ] || [ -L $1 ]\n}\n\ncp_resolv_conf() {\n    os_dir=$1\n    if is_file_or_link $os_dir/etc/resolv.conf &&\n        ! is_file_or_link $os_dir/etc/resolv.conf.orig; then\n        mv $os_dir/etc/resolv.conf $os_dir/etc/resolv.conf.orig\n    fi\n    cp -f /etc/resolv.conf $os_dir/etc/resolv.conf\n}\n\nrm_resolv_conf() {\n    os_dir=$1\n    rm -f $os_dir/etc/resolv.conf $os_dir/etc/resolv.conf.orig\n}\n\nrestore_resolv_conf() {\n    os_dir=$1\n    if is_file_or_link $os_dir/etc/resolv.conf.orig; then\n        mv -f $os_dir/etc/resolv.conf.orig $os_dir/etc/resolv.conf\n    fi\n}\n\nkeep_now_resolv_conf() {\n    os_dir=$1\n    rm -f $os_dir/etc/resolv.conf.orig\n}\n\n# 抄 https://github.com/alpinelinux/alpine-conf/blob/3.18.1/setup-disk.in#L421\nget_alpine_firmware_pkgs() {\n    # 需要有 modloop，不然 modinfo 会报错\n    ensure_service_started modloop >&2\n\n    # 如果不在单独的文件夹，则用 linux-firmware-other\n    # 如果在单独的文件夹，则用 linux-firmware-xxx\n    # 如果不需要 firmware，则用 linux-firmware-none\n    firmware_pkgs=$(\n        cd /sys/module && modinfo -F firmware -- * 2>/dev/null |\n            awk -F/ '{print $1 == $0 ? \"linux-firmware-other\" : \"linux-firmware-\"$1}' |\n            sort -u\n    )\n\n    # 使用 command 因为自己覆盖了 apk 添加了 >&2\n    retry 5 command apk search --quiet --exact ${firmware_pkgs:-linux-firmware-none}\n}\n\nget_ucode_firmware_pkgs() {\n    is_virt && return\n\n    case \"$distro\" in\n    centos | almalinux | rocky | oracle | redhat | anolis | opencloudos | openeuler) os=elol ;;\n    *) os=$distro ;;\n    esac\n\n    case \"$os-$(get_cpu_vendor)\" in\n    # alpine 的 linux-firmware 以文件夹进行拆分\n    # setup-alpine 会自动安装需要的 firmware（modloop 没挂载则无效）\n    # https://github.com/alpinelinux/alpine-conf/blob/3.18.1/setup-disk.in#L421\n    alpine-intel) echo intel-ucode ;;\n    alpine-amd) echo amd-ucode ;;\n    alpine-*) ;;\n\n    debian-intel) echo firmware-linux intel-microcode ;;\n    debian-amd) echo firmware-linux amd64-microcode ;;\n    debian-*) echo firmware-linux ;;\n\n    ubuntu-intel) echo linux-firmware intel-microcode ;;\n    ubuntu-amd) echo linux-firmware amd64-microcode ;;\n    ubuntu-*) echo linux-firmware ;;\n\n    # 无法同时安装 kernel-firmware kernel-firmware-intel\n    opensuse-intel) echo kernel-firmware ucode-intel ;;\n    opensuse-amd) echo kernel-firmware ucode-amd ;;\n    opensuse-*) echo kernel-firmware ;;\n\n    arch-intel) echo linux-firmware intel-ucode ;;\n    arch-amd) echo linux-firmware amd-ucode ;;\n    arch-*) echo linux-firmware ;;\n\n    gentoo-intel) echo linux-firmware intel-microcode ;;\n    gentoo-amd) echo linux-firmware ;;\n    gentoo-*) echo linux-firmware ;;\n\n    nixos-intel) echo linux-firmware microcodeIntel ;;\n    nixos-amd) echo linux-firmware microcodeAmd ;;\n    nixos-*) echo linux-firmware ;;\n\n    fedora-intel) echo linux-firmware microcode_ctl ;;\n    fedora-amd) echo linux-firmware amd-ucode-firmware microcode_ctl ;;\n    fedora-*) echo linux-firmware microcode_ctl ;;\n\n    elol-intel) echo linux-firmware microcode_ctl ;;\n    elol-amd) echo linux-firmware microcode_ctl ;;\n    elol-*) echo linux-firmware microcode_ctl ;;\n    esac\n}\n\nchroot_systemctl_disable() {\n    os_dir=$1\n    shift\n\n    for unit in \"$@\"; do\n        # 如果传进来的是x(没有.) 则改成 x.service\n        if ! [[ \"$unit\" = \"*.*\" ]]; then\n            unit=$i.service\n        fi\n\n        # debian 10 返回值始终是 0\n        if ! chroot $os_dir systemctl list-unit-files \"$unit\" 2>&1 | grep -Eq '^0 unit'; then\n            chroot $os_dir systemctl disable \"$unit\"\n        fi\n    done\n}\n\nremove_or_disable_cloud_init() {\n    os_dir=$1\n\n    if ! is_have_cmd_on_disk $os_dir cloud-init; then\n        return\n    fi\n\n    info \"Remove or Disable Cloud-Init\"\n\n    # ubuntu-server-minimal ubuntu-cloud-minimal 都包含 cloud-init\n    # 用 iso 安装的 ubuntu 也有 cloud-init\n    # 因此不删除 ubuntu 的 cloud-init，而是禁用它\n\n    # iso 安装首次启动是通过 /etc/cloud/cloud.cfg.d/99-installer.cfg 初始化系统，包括：\n    #     1. 创建普通用户和密码，添加 ssh 登录公钥\n    #     2. 创建 /etc/cloud/cloud-init.disabled\n\n    if grep -iq ubuntu $os_dir/etc/os-release; then\n        # 模仿 iso 安装的 ubuntu，只创建 cloud-init.disabled，不禁用服务\n        touch $os_dir/etc/cloud/cloud-init.disabled\n    else\n        # systemctl is-enabled cloud-init-hotplugd.service 状态是 static\n        # disable 会出现一堆提示信息，也无法 disable\n        for unit in $(\n            chroot $os_dir systemctl list-unit-files |\n                grep -E '^(cloud-init|cloud-init-.*|cloud-config|cloud-final)\\.(service|socket)' | grep enabled | awk '{print $1}'\n        ); do\n            # 服务不存在时会报错\n            if chroot $os_dir systemctl -q is-enabled \"$unit\"; then\n                chroot $os_dir systemctl disable \"$unit\"\n            fi\n        done\n\n        for pkg_mgr in dnf yum zypper apt-get; do\n            if is_have_cmd_on_disk $os_dir $pkg_mgr; then\n                case $pkg_mgr in\n                dnf | yum)\n                    chroot $os_dir $pkg_mgr remove -y cloud-init\n                    rm -f $os_dir/etc/cloud/cloud.cfg.rpmsave\n                    ;;\n                zypper)\n                    # 加上 -u 才会删除依赖\n                    chroot $os_dir zypper remove -y -u cloud-init cloud-init-config-suse\n                    ;;\n                apt-get)\n                    # ubuntu 25.04 开始有 cloud-init-base\n                    chroot_apt_remove $os_dir cloud-init cloud-init-base\n                    chroot_apt_autoremove $os_dir\n                    ;;\n                esac\n                break\n            fi\n        done\n    fi\n}\n\ndisable_jeos_firstboot() {\n    os_dir=$1\n    info \"Disable JeOS Firstboot\"\n\n    # 两种方法都可以\n    # https://github.com/openSUSE/jeos-firstboot?tab=readme-ov-file#usage\n\n    rm -rf $os_dir/var/lib/YaST2/reconfig_system\n\n    for name in jeos-firstboot jeos-firstboot-snapshot; do\n        # 服务不存在时会报错\n        chroot $os_dir systemctl disable \"$name.service\" 2>/dev/null || true\n    done\n\n    # 可选\n    # chroot $os_dir zypper remove -y -u jeos-firstboot\n}\n\ncreate_network_manager_config() {\n    source_cfg=$1\n    os_dir=$2\n    info \"Create Network-Manager config\"\n\n    # 可以直接用 alpine 的 cloud-init 生成 Network Manager 配置\n    apk add cloud-init\n    cloud-init devel net-convert -p \"$source_cfg\" -k yaml -d /out -D alpine -O network-manager\n\n    # 文档明确写了 ipv6.method=dhcp 无法获取网关\n    # https://networkmanager.dev/docs/api/latest/nm-settings-nmcli.html#:~:text=false/no/off-,ipv6,-.method\n    sed -i -e '/^may-fail=/d' -e 's/^method=dhcp/method=auto/' \\\n        /out/etc/NetworkManager/system-connections/cloud-init-eth*.nmconnection\n\n    # 删除 # Generated by cloud-init. Changes will be lost.\n    # 删除 org.freedesktop.NetworkManager.origin=cloud-init\n    # 并删除头部的空行\n    sed -i \\\n        -e '/^# Generated by cloud-init/d' \\\n        -e '/^org\\.freedesktop\\.NetworkManager\\.origin=cloud-init/d' \\\n        /out/etc/NetworkManager/system-connections/cloud-init-eth*.nmconnection\n    del_head_empty_lines_inplace /out/etc/NetworkManager/system-connections/cloud-init-eth*.nmconnection\n\n    cp /out/etc/NetworkManager/system-connections/cloud-init-eth*.nmconnection \\\n        $os_dir/etc/NetworkManager/system-connections/\n\n    # 清理\n    rm -rf /out\n    apk del cloud-init\n\n    # 最终显示文件\n    for file in \"$os_dir\"/etc/NetworkManager/system-connections/cloud-init-eth*.nmconnection; do\n        cat -n \"$file\" >&2\n    done\n}\n\nmodify_linux() {\n    os_dir=$1\n    info \"Modify Linux\"\n\n    find_and_mount() {\n        mount_point=$1\n        mount_dev=$(awk \"\\$2==\\\"$mount_point\\\" {print \\$1}\" $os_dir/etc/fstab)\n        if [ -n \"$mount_dev\" ]; then\n            mount $mount_dev $os_dir$mount_point\n        fi\n    }\n\n    # 修复 onlink 网关\n    add_onlink_script_if_need() {\n        for ethx in $(get_eths); do\n            if is_staticv4 || is_staticv6; then\n                fix_sh=cloud-init-fix-onlink.sh\n                download \"$confhome/$fix_sh\" \"$os_dir/$fix_sh\"\n                insert_into_file \"$ci_file\" after '^runcmd:' <<EOF\n  - bash \"/$fix_sh\" && rm -f \"/$fix_sh\"\nEOF\n                break\n            fi\n        done\n    }\n\n    # 部分镜像有默认配置，例如 centos\n    del_exist_sysconfig_NetworkManager_config $os_dir\n\n    # 仅 fedora (el/ol/国产fork 用的是复制文件方法)\n    # 1. 禁用 selinux kdump\n    # 2. 添加微码+固件\n    if [ -f $os_dir/etc/redhat-release ]; then\n        # 防止删除 cloud-init / 安装 firmware 时不够内存\n        create_swap_if_ram_less_than 2048 $os_dir/swapfile\n\n        find_and_mount /boot\n        find_and_mount /boot/efi\n        mount_pseudo_fs $os_dir\n        cp_resolv_conf $os_dir\n\n        # 可以直接用 alpine 的 cloud-init 生成 Network Manager 配置\n        create_cloud_init_network_config /net.cfg\n        create_network_manager_config /net.cfg \"$os_dir\"\n        rm /net.cfg\n\n        # TODO: fedora 43 eol 后删除\n        # 删除 cloud-init 会删除依赖包 netcat\n        # 但是删除 netcat 时会报错\n        # 因此保留 netcat 包\n        # >>> Running %preun scriptlet: netcat-0:1.229-3.fc43.x86_64\n        # >>> Error in %preun scriptlet: netcat-0:1.229-3.fc43.x86_64\n        # >>> Scriptlet output:\n        # >>> failed to create admindir: No such file or directory\n        # >>> [RPM] %preun(netcat-1.229-3.fc43.x86_64) scriptlet failed, exit status 2\n        # >>> [RPM] netcat-1.229-3.fc43.x86_64: erase failed\n        if [ \"$distro\" = fedora ] && [ \"$releasever\" = 43 ]; then\n            chroot $os_dir dnf mark user netcat -y\n        fi\n        remove_or_disable_cloud_init $os_dir\n\n        disable_selinux $os_dir\n        disable_kdump $os_dir\n\n        if fw_pkgs=$(get_ucode_firmware_pkgs) && [ -n \"$fw_pkgs\" ]; then\n            is_have_cmd_on_disk $os_dir dnf && mgr=dnf || mgr=yum\n            chroot $os_dir $mgr install -y $fw_pkgs\n        fi\n\n        restore_resolv_conf $os_dir\n    fi\n\n    # debian\n    # 1. EOL 换源\n    # 2. 修复网络问题\n    # 3. 添加微码+固件\n    # 注意 ubuntu 也有 /etc/debian_version\n    if [ \"$distro\" = debian ]; then\n        # 修复 onlink 网关\n        # add_onlink_script_if_need\n\n        mount_pseudo_fs $os_dir\n        cp_resolv_conf $os_dir\n        find_and_mount /boot\n        find_and_mount /boot/efi\n\n        remove_or_disable_cloud_init $os_dir\n\n        # 获取当前开启的 Components, 后面要用\n        if [ -f $os_dir/etc/apt/sources.list.d/debian.sources ]; then\n            comps=$(grep ^Components: $os_dir/etc/apt/sources.list.d/debian.sources | head -1 | cut -d' ' -f2-)\n        else\n            comps=$(grep '^deb ' $os_dir/etc/apt/sources.list | head -1 | cut -d' ' -f4-)\n        fi\n\n        # ELTS/CN 源处理\n        if is_elts; then\n            # ELTS\n            wget https://deb.freexian.com/extended-lts/archive-key.gpg \\\n                -O $os_dir/etc/apt/trusted.gpg.d/freexian-archive-extended-lts.gpg\n\n            # shellcheck disable=SC1091\n            codename=$({ . \"$os_dir/etc/os-release\" && echo \"$VERSION_CODENAME\"; })\n            if [ -f $os_dir/etc/apt/sources.list.d/debian.sources ]; then\n                cat <<EOF >$os_dir/etc/apt/sources.list.d/debian.sources\nTypes: deb\nURIs: http://$deb_mirror\nSuites: $codename\nComponents: $comps\nSigned-By: /etc/apt/trusted.gpg.d/freexian-archive-extended-lts.gpg\nEOF\n            else\n                echo \"deb http://$deb_mirror $codename $comps\" >$os_dir/etc/apt/sources.list\n            fi\n        else\n            # non-ELTS\n            if is_in_china; then\n                # 不处理 security 源 security.debian.org/debian-security 和 /etc/apt/mirrors/debian-security.list\n                for file in $os_dir/etc/apt/mirrors/debian.list $os_dir/etc/apt/sources.list; do\n                    if [ -f \"$file\" ]; then\n                        sed -i \"s|deb\\.debian\\.org/debian|$deb_mirror|\" \"$file\"\n                    fi\n                done\n            fi\n        fi\n\n        # 标记所有内核为自动安装\n        pkgs=$(chroot $os_dir apt-mark showmanual linux-image* linux-headers*)\n        chroot $os_dir apt-mark auto $pkgs\n\n        # 安装合适的内核\n        kernel_package=$kernel\n        # shellcheck disable=SC2046\n        # 检测机器是否能用 cloud 内核\n        if [[ \"$kernel_package\" = 'linux-image-cloud-*' ]] &&\n            ! sh /can_use_cloud_kernel.sh \"$xda\" $(get_eths); then\n            kernel_package=$(echo \"$kernel_package\" | sed 's/-cloud//')\n        fi\n\n        # 该方法包含了 apt-mark manual\n        chroot_apt_install $os_dir \"$kernel_package\"\n\n        # 使用 autoremove 删除非最佳内核\n        chroot_apt_autoremove $os_dir\n\n        # 微码+固件\n        if fw_pkgs=$(get_ucode_firmware_pkgs) && [ -n \"$fw_pkgs\" ]; then\n            #  debian 10 11 的 iucode-tool 在 contrib 里面\n            #  debian 12 的 iucode-tool 在 main 里面\n            [ \"$releasever\" -ge 12 ] &&\n                comps_to_add=non-free-firmware ||\n                comps_to_add=\"contrib non-free\"\n\n            if [ -f $os_dir/etc/apt/sources.list.d/debian.sources ]; then\n                file=$os_dir/etc/apt/sources.list.d/debian.sources\n                search='^[# ]*Components:'\n            else\n                file=$os_dir/etc/apt/sources.list\n                search='^[# ]*deb'\n            fi\n\n            for c in $comps_to_add; do\n                if ! echo \"$comps\" | grep -wq \"$c\"; then\n                    sed -Ei \"/$search/s/$/ $c/\" $file\n                fi\n            done\n\n            chroot_apt_install $os_dir $fw_pkgs\n        fi\n\n        # genericcloud 删除以下文件开机时才会显示 grub 菜单\n        # https://salsa.debian.org/cloud-team/debian-cloud-images/-/tree/master/config_space/bookworm/files/etc/default/grub.d\n        rm -f $os_dir/etc/default/grub.d/10_cloud.cfg\n        rm -f $os_dir/etc/default/grub.d/15_timeout.cfg\n        chroot $os_dir update-grub\n\n        if true; then\n            # 如果使用 nocloud 镜像\n            chroot_apt_install $os_dir openssh-server\n        else\n            # 如果使用 genericcloud 镜像\n\n            # 还原默认配置并创建 key\n            # cat $os_dir/usr/share/openssh/sshd_config $os_dir/etc/ssh/sshd_config\n            # chroot $os_dir ssh-keygen -A\n            rm -rf $os_dir/etc/ssh/sshd_config\n            UCF_FORCE_CONFFMISS=1 chroot $os_dir dpkg-reconfigure openssh-server\n        fi\n\n        # 镜像自带的网络管理器\n        # debian 11 ifupdown\n        # debian 12 netplan + networkd + resolved\n        # ifupdown dhcp 不支持 24位掩码+不规则网关?\n\n        # 强制使用 netplan\n        if false && is_have_cmd_on_disk $os_dir netplan; then\n            chroot_apt_install $os_dir netplan.io\n            # 服务不存在时会报错\n            chroot $os_dir systemctl disable networking resolvconf 2>/dev/null || true\n            chroot $os_dir systemctl enable systemd-networkd systemd-resolved\n            rm_resolv_conf $os_dir\n            ln -sf ../run/systemd/resolve/stub-resolv.conf $os_dir/etc/resolv.conf\n            if [ -f \"$os_dir/etc/cloud/cloud.cfg.d/99_fallback.cfg\" ]; then\n                insert_into_file $os_dir/etc/cloud/cloud.cfg.d/99_fallback.cfg after '#cloud-config' <<EOF\nsystem_info:\n  network:\n    renderers: [netplan]\n    activators: [netplan]\nEOF\n            fi\n        fi\n\n        create_ifupdown_config $os_dir/etc/network/interfaces\n\n        # ifupdown 不支持 rdnss\n        # 但 iso 安装不会安装 rdnssd，而是在安装时读取 rdnss 并写入 resolv.conf\n        if false; then\n            chroot_apt_install $os_dir rdnssd\n        fi\n\n        # debian 10 11 云镜像安装了 resolvconf\n        # debian 12 云镜像安装了 netplan systemd-resolved\n        # 云镜像用了 cloud-init 自动配置网络，用户是无感的，因此官方云镜像可以随便选择网络管理器\n        # 但我们的系统安装后用户可能有手动配置网络的需求，因此用回 iso 安装时的网络管理器 ifupdown\n\n        # 服务不存在时会报错\n        chroot $os_dir systemctl disable resolvconf systemd-networkd systemd-resolved 2>/dev/null || true\n\n        chroot_apt_install $os_dir ifupdown\n        chroot_apt_remove $os_dir resolvconf netplan.io systemd-resolved\n        chroot_apt_autoremove $os_dir\n        chroot $os_dir systemctl enable networking\n\n        # 静态时 networking 服务不会根据 /etc/network/interfaces 更新 resolv.conf\n        # 动态时使用了 isc-dhcp-client 支持自动更新 resolv.conf\n        # 另外 debian iso 不会安装 rdnssd\n        keep_now_resolv_conf $os_dir\n    fi\n\n    # opensuse\n    # 1. kernel-default-base 缺少 nvme gve mlx5 mana 驱动，换成 kernel-default\n    # 2. 添加微码+固件\n    # https://documentation.suse.com/smart/virtualization-cloud/html/minimal-vm/index.html\n    if grep -q opensuse $os_dir/etc/os-release; then\n        create_swap_if_ram_less_than 1024 $os_dir/swapfile\n        mount_pseudo_fs $os_dir\n        cp_resolv_conf $os_dir\n        find_and_mount /boot\n        find_and_mount /boot/efi\n\n        disable_jeos_firstboot $os_dir\n\n        # 禁用 selinux\n        disable_selinux $os_dir\n\n        # opensuse leap 15.6 用 wicked\n        # opensuse leap 16.0 / tumbleweed 用 NetworkManager\n        if chroot $os_dir rpm -qi wicked; then\n            # sysconfig ifcfg\n            create_cloud_init_network_config $os_dir/net.cfg\n            chroot $os_dir cloud-init devel net-convert \\\n                -p /net.cfg -k yaml -d out -D opensuse -O sysconfig\n\n            # 删除\n            # Created by cloud-init on instance boot automatically, do not edit.\n            #\n            sed -i '/^#/d' \"$os_dir/out/etc/sysconfig/network/ifcfg-eth\"*\n\n            for ethx in $(get_eths); do\n                # 1. 修复甲骨文云重启后 ipv6 丢失\n                # https://github.com/openSUSE/wicked/issues/1058\n                # 还要注意 wicked dhcpv6 获取到的 ipv6 是 /64，其他 DHCPv6 程序获取到的是 /128\n                echo DHCLIENT6_USE_LAST_LEASE=no >>$os_dir/out/etc/sysconfig/network/ifcfg-$ethx\n\n                # 2. 修复 onlink 网关\n                for prefix in '' 'default '; do\n                    if is_staticv4; then\n                        get_netconf_to ipv4_gateway\n                        echo \"${prefix}${ipv4_gateway} - -\" >>$os_dir/out/etc/sysconfig/network/ifroute-$ethx\n                    fi\n                    if is_staticv6; then\n                        get_netconf_to ipv6_gateway\n                        echo \"${prefix}${ipv6_gateway} - -\" >>$os_dir/out/etc/sysconfig/network/ifroute-$ethx\n                    fi\n                done\n            done\n\n            # 复制配置\n            for file in \\\n                \"$os_dir/out/etc/sysconfig/network/ifcfg-eth\"* \\\n                \"$os_dir/out/etc/sysconfig/network/ifroute-eth\"*; do\n                # 动态 ip 没有 ifroute-eth*\n                if [ -f $file ]; then\n                    cp $file $os_dir/etc/sysconfig/network/\n                fi\n            done\n\n            # 清理\n            rm -rf $os_dir/net.cfg $os_dir/out\n\n        else\n            # 如果使用 cloud-init 则需要 touch NetworkManager.conf\n            # 更新到 cloud-init 24.1 后删除\n            # touch $os_dir/etc/NetworkManager/NetworkManager.conf\n\n            # 可以直接用 alpine 的 cloud-init 生成 Network Manager 配置\n            create_cloud_init_network_config /net.cfg\n            create_network_manager_config /net.cfg \"$os_dir\"\n            rm /net.cfg\n        fi\n\n        # 选择新内核\n        # 只有 leap 有 kernel-azure\n        if grep -iq leap $os_dir/etc/os-release && [ \"$(get_cloud_vendor)\" = azure ]; then\n            target_kernel='kernel-azure'\n        else\n            target_kernel='kernel-default'\n        fi\n\n        # rpm -qi 不支持通配符\n        origin_kernel=$(chroot $os_dir rpm -qa 'kernel-*' --qf '%{NAME}\\n' | grep -v firmware)\n        if ! [ \"$(echo \"$origin_kernel\" | wc -l)\" -eq 1 ]; then\n            error_and_exit \"Unexpected kernel installed: $origin_kernel\"\n        fi\n\n        # 16.0 能同时装 kernel-default-base 和 kernel-default\n        # tw 不能同时装 kernel-default-base 和 kernel-default\n        # 因此需要添加 --force-resolution 自动删除 kernel-default-base\n        if ! [ \"$origin_kernel\" = \"$target_kernel\" ]; then\n            # x86 必须设置一个密码，否则报错，arm 没有这个问题\n            # Failed to get root password hash\n            # Failed to import /etc/uefi/certs/76B6A6A0.crt\n            # warning: %post(kernel-default-5.14.21-150500.55.83.1.x86_64) scriptlet failed, exit status 255\n            need_password_workaround=false\n            if grep -q '^root:[:!*]' $os_dir/etc/shadow; then\n                need_password_workaround=true\n            fi\n\n            if $need_password_workaround; then\n                echo \"root:$(mkpasswd '')\" | chroot $os_dir chpasswd -e\n            fi\n            # 安装新内核\n            chroot $os_dir zypper install -y --force-resolution $target_kernel\n            # 删除旧内核\n            if chroot $os_dir rpm -q $origin_kernel; then\n                chroot $os_dir zypper remove -y --force-resolution $origin_kernel\n            fi\n            if $need_password_workaround; then\n                chroot $os_dir passwd -d root\n            fi\n        fi\n\n        # 固件+微码\n        if fw_pkgs=$(get_ucode_firmware_pkgs) && [ -n \"$fw_pkgs\" ]; then\n            chroot $os_dir zypper install -y $fw_pkgs\n        fi\n\n        # 最后才删除 cloud-init\n        # 因为生成 sysconfig 网络配置要用目标系统的 cloud-init\n        remove_or_disable_cloud_init $os_dir\n\n        restore_resolv_conf $os_dir\n    fi\n\n    # arch 云镜像\n    if false && [ -f $os_dir/etc/arch-release ]; then\n        # 修复 onlink 网关\n        add_onlink_script_if_need\n\n        # 同步证书\n        cp_resolv_conf $os_dir\n        mount_pseudo_fs $os_dir\n        chroot $os_dir pacman-key --init\n        chroot $os_dir pacman-key --populate\n        rm_resolv_conf $os_dir\n    fi\n\n    # gentoo 云镜像\n    if false && [ -f $os_dir/etc/gentoo-release ]; then\n        # 挂载伪文件系统\n        mount_pseudo_fs $os_dir\n        cp_resolv_conf $os_dir\n\n        # 在这里修改密码，而不是用cloud-init，因为我们的默认密码太弱\n        is_password_plaintext && sed -i 's/enforce=everyone/enforce=none/' $os_dir/etc/security/passwdqc.conf\n        change_root_password $os_dir\n        is_password_plaintext && sed -i 's/enforce=none/enforce=everyone/' $os_dir/etc/security/passwdqc.conf\n\n        # 下载仓库，选择 profile\n        chroot $os_dir emerge-webrsync\n        profile=$(chroot $os_dir eselect profile list | grep stable | grep systemd |\n            awk '{print length($2), $2}' | sort -n | head -1 | awk '{print $2}')\n        chroot $os_dir eselect profile set $profile\n\n        # 删除 resolv.conf，不然 systemd-resolved 无法创建软链接\n        rm_resolv_conf $os_dir\n\n        # 启用网络服务\n        chroot $os_dir systemctl enable systemd-networkd\n        chroot $os_dir systemctl enable systemd-resolved\n\n        # systemd-networkd 有时不会运行\n        # https://bugs.gentoo.org/910404 补丁好像没用\n        # https://github.com/systemd/systemd/issues/27718#issuecomment-1564877478\n        # 临时的解决办法是运行 networkctl，如果启用了systemd-networkd服务，会运行服务\n        insert_into_file $os_dir/lib/systemd/system/systemd-logind.service after '\\[Service\\]' <<EOF\nExecStartPost=-networkctl\nEOF\n\n        # 如果创建了 cloud-init.disabled，重启后网络不受 networkd 管理\n        # 因为网卡名变回了 ens3 而不是 eth0\n        # 因此要删除 networkd 的网卡名匹配\n        insert_into_file $ci_file after '^runcmd:' <<EOF\n  - sed -i '/^Name=/d' /etc/systemd/network/10-cloud-init-eth*.network\nEOF\n\n        # 修复 onlink 网关\n        add_onlink_script_if_need\n    fi\n\n    basic_init $os_dir\n\n    # 应该在这里是否运行了 basic_init 和创建了网络配置文件\n    # 如果没有，则使用 cloud-init\n\n    # 查看 cloud-init 最终配置\n    if [ -f \"$ci_file\" ]; then\n        cat -n \"$ci_file\"\n    fi\n\n    # 删除 swap\n    swapoff -a\n    rm -f $os_dir/swapfile\n}\n\nsetup_nocloud() {\n    os_dir=$1\n    info \"Setup NoCloud\"\n\n    # 1. 配置 NoCloud-only datasource\n    mkdir -p \"$os_dir/etc/cloud/cloud.cfg.d\"\n    cat >\"$os_dir/etc/cloud/cloud.cfg.d/99-datasource.cfg\" <<'EOF'\ndatasource_list: [ NoCloud, None ]\ndatasource:\n  NoCloud:\n    seedfrom: /var/lib/cloud/seed/nocloud/\n    fs_label: null\nEOF\n\n    # 2. 复制 seed 文件（已在 host 上准备好，打包在 initrd 中）\n    mkdir -p \"$os_dir/var/lib/cloud/seed/nocloud\"\n    cp /configs/cloud-data/* \"$os_dir/var/lib/cloud/seed/nocloud/\"\n\n    # 3. 确保 cloud-init 没有被禁用\n    rm -f \"$os_dir/etc/cloud/cloud-init.disabled\"\n\n    # 4. 清除 cloud-init 旧状态，确保首次启动重新执行\n    rm -rf \"$os_dir/var/lib/cloud/instance\"\n    rm -rf \"$os_dir/var/lib/cloud/instances\"\n}\n\nmodify_os_on_disk() {\n    only_process=$1\n    info \"Modify disk if is $only_process\"\n\n    update_part\n\n    # dd linux 的时候不用修改硬盘内容（nocloud 模式除外）\n    if [ \"$distro\" = \"dd\" ] && [ \"$only_process\" != \"nocloud\" ] && ! lsblk -f /dev/$xda | grep ntfs; then\n        return\n    fi\n\n    mkdir -p /os\n    # 按分区容量大到小，依次寻找系统分区\n    for part in $(lsblk /dev/$xda*[0-9] --sort SIZE -no NAME | tac); do\n        # btrfs挂载的是默认子卷，如果没有默认子卷，挂载的是根目录\n        # fedora 云镜像没有默认子卷，且系统在root子卷中\n        if mount -o ro /dev/$part /os; then\n            if [ \"$only_process\" = linux ] || [ \"$only_process\" = nocloud ]; then\n                if etc_dir=$({ ls -d /os/etc/ || ls -d /os/*/etc/; } 2>/dev/null); then\n                    os_dir=$(dirname $etc_dir)\n                    # 重新挂载为读写\n                    mount -o remount,rw /os\n                    if [ \"$only_process\" = nocloud ]; then\n                        setup_nocloud $os_dir\n                    else\n                        modify_linux $os_dir\n                    fi\n                    return\n                fi\n            elif [ \"$only_process\" = windows ]; then\n                # find 不是很聪明\n                # find /mnt/c -iname windows -type d -maxdepth 1\n                # find: /mnt/c/pagefile.sys: Permission denied\n                # find: /mnt/c/swapfile.sys: Permission denied\n                # shellcheck disable=SC1090\n                # find_file_ignore_case 也在这个文件里面\n                . <(wget -O- $confhome/windows-driver-utils.sh)\n                if find_file_ignore_case /os/Windows/System32/ntoskrnl.exe >/dev/null 2>&1; then\n                    # 其他地方会用到\n                    is_windows() { true; }\n                    # 重新挂载为读写、忽略大小写\n                    umount /os\n                    if ! { mount -t ntfs3 -o nocase,rw /dev/$part /os &&\n                        mount | grep -w 'on /os type' | grep -wq rw; }; then\n                        # 显示警告\n                        warn \"Can't normally mount windows partition /dev/$part as rw.\"\n                        dmesg | grep -F \"ntfs3($part):\" || true\n                        # 有可能 fallback 挂载成 ro, 因此先取消挂载\n                        if mount | grep -wq 'on /os type'; then\n                            umount /os\n                        fi\n                        # 尝试修复并强制挂载\n                        apk add ntfs-3g-progs\n                        ntfsfix /dev/$part\n                        apk del ntfs-3g-progs\n                        mount -t ntfs3 -o nocase,rw,force /dev/$part /os\n                    fi\n                    # 获取版本号，其他地方会用到\n                    get_windows_version_from_windows_drive /os\n                    modify_windows /os\n                    return\n                fi\n            fi\n            umount /os\n        fi\n    done\n    error_and_exit \"Can't find os partition.\"\n}\n\nget_need_swap_size() {\n    need_ram=$1\n    phy_ram=$(get_approximate_ram_size)\n\n    if [ $need_ram -gt $phy_ram ]; then\n        echo $((need_ram - phy_ram))\n    else\n        echo 0\n    fi\n}\n\ncreate_swap_if_ram_less_than() {\n    need_ram=$1\n    swapfile=$2\n\n    swapsize=$(get_need_swap_size $need_ram)\n    if [ $swapsize -gt 0 ]; then\n        create_swap $swapsize $swapfile\n    fi\n}\n\ncreate_swap() {\n    swapsize=$1\n    swapfile=$2\n\n    if ! grep $swapfile /proc/swaps; then\n        # 用兼容 btrfs 的方式创建 swapfile\n        truncate -s 0 $swapfile\n        # 如果分区不支持 chattr +C 会显示错误但返回值是 0\n        chattr +C $swapfile 2>/dev/null\n        fallocate -l ${swapsize}M $swapfile\n        chmod 0600 $swapfile\n        mkswap $swapfile\n        swapon $swapfile\n    fi\n}\n\nset_ssh_keys_and_del_password() {\n    os_dir=$1\n    info 'set ssh keys'\n\n    # 添加公钥\n    (\n        umask 077\n        mkdir -p $os_dir/root/.ssh\n        cat /configs/ssh_keys >$os_dir/root/.ssh/authorized_keys\n    )\n\n    # 删除密码\n    chroot $os_dir passwd -d root\n}\n\n# 除了 alpine 都会用到\nchange_ssh_conf() {\n    os_dir=$1\n    key=$2\n    value=$3\n    sub_conf=$4\n\n    if line=\"^$key .*\" && grep -Exq \"$line\" $os_dir/etc/ssh/sshd_config 2>/dev/null; then\n        # 如果 sshd_config 存在此 key（非注释状态），则替换\n        sed -Ei \"s/$line/$key $value/\" $os_dir/etc/ssh/sshd_config\n    elif include_line='^Include.*/etc/ssh/sshd_config.d' &&\n        # arch 没有 /etc/ssh/sshd_config.d/ 文件夹\n        # opensuse tumbleweed 没有 /etc/ssh/sshd_config\n        #                       有 /etc/ssh/sshd_config.d/ 文件夹\n        #                       有 /usr/etc/ssh/sshd_config\n        { grep -q \"$include_line\" $os_dir/etc/ssh/sshd_config ||\n            grep -q \"$include_line\" $os_dir/usr/etc/ssh/sshd_config; } 2>/dev/null; then\n        mkdir -p $os_dir/etc/ssh/sshd_config.d/\n        echo \"$key $value\" >\"$os_dir/etc/ssh/sshd_config.d/$sub_conf\"\n    else\n        # 如果 sshd_config 存在此 key (无论是否已注释)，则替换，包括删除注释\n        # 否则追加\n        line=\"^[# ]*$key .*\"\n        if grep -Exq \"$line\" $os_dir/etc/ssh/sshd_config; then\n            sed -Ei \"s/$line/$key $value/\" $os_dir/etc/ssh/sshd_config\n        else\n            echo \"$key $value\" >>$os_dir/etc/ssh/sshd_config\n        fi\n    fi\n}\n\nallow_password_login() {\n    os_dir=$1\n    change_ssh_conf \"$os_dir\" PasswordAuthentication yes 01-PasswordAuthentication.conf\n}\n\nallow_root_password_login() {\n    os_dir=$1\n\n    # opensuse 16/tumbleweed 安装 openssh-server-config-rootlogin\n    # 会生成 /usr/etc/ssh/sshd_config.d/50-permit-root-login.conf\n    # 但是如果用户删除了此文件，包有更新的话，可能会重新创建这个文件？\n    # 因此先不用这个方法\n    if false && [ -f $os_dir/etc/os-release ] &&\n        grep -iq opensuse $os_dir/etc/os-release &&\n        ! grep -iq 15.6 $os_dir/etc/os-release; then\n        chroot $os_dir zypper install -y openssh-server-config-rootlogin\n    else\n        change_ssh_conf \"$os_dir\" PermitRootLogin yes 01-permitrootlogin.conf\n    fi\n}\n\nchange_ssh_port() {\n    os_dir=$1\n    ssh_port=$2\n\n    change_ssh_conf \"$os_dir\" Port \"$ssh_port\" 01-change-ssh-port.conf\n}\n\nchange_root_password() {\n    os_dir=$1\n\n    info 'change root password'\n\n    if is_password_plaintext; then\n        pam_d=$os_dir/etc/pam.d\n\n        [ -f $pam_d/chpasswd ] && has_pamd_chpasswd=true || has_pamd_chpasswd=false\n\n        if $has_pamd_chpasswd; then\n            cp $pam_d/chpasswd $pam_d/chpasswd.orig\n\n            # cat /etc/pam.d/chpasswd\n            # @include common-password\n\n            # cat /etc/pam.d/chpasswd\n            # #%PAM-1.0\n            # auth       include      system-auth\n            # account    include      system-auth\n            # password   substack     system-auth\n            # -password   optional    pam_gnome_keyring.so use_authtok\n            # password   substack     postlogin\n\n            # 通过 /etc/pam.d/chpasswd 找到 /etc/pam.d/system-auth 或者 /etc/pam.d/system-auth\n            # 再找到有 password 和 pam_unix.so 的行，并删除 use_authtok，写入 /etc/pam.d/chpasswd\n            files=$(grep -E '^(password|@include)' $pam_d/chpasswd | awk '{print $NF}' | sort -u)\n            for file in $files; do\n                if [ -f \"$pam_d/$file\" ] && line=$(grep ^password \"$pam_d/$file\" | grep -F pam_unix.so); then\n                    echo \"$line\" | sed 's/use_authtok//' >$pam_d/chpasswd\n                    break\n                fi\n            done\n        fi\n\n        # 分两行写，不然遇到错误不会终止\n        plaintext=$(get_password_plaintext)\n        echo \"root:$plaintext\" | chroot $os_dir chpasswd\n\n        if $has_pamd_chpasswd; then\n            mv $pam_d/chpasswd.orig $pam_d/chpasswd\n        fi\n    else\n        echo \"root:$(get_password_linux_sha512)\" | chroot $os_dir chpasswd -e\n    fi\n}\n\ndisable_selinux() {\n    os_dir=$1\n\n    # https://access.redhat.com/solutions/3176\n    # centos7 也建议将 selinux 开关写在 cmdline\n    # grep selinux=0 /usr/lib/dracut/modules.d/98selinux/selinux-loadpolicy.sh\n    #     warn \"To disable selinux, add selinux=0 to the kernel command line.\"\n    if [ -f $os_dir/etc/selinux/config ]; then\n        sed -i 's/^SELINUX=enforcing/SELINUX=disabled/g' $os_dir/etc/selinux/config\n    fi\n\n    # opensuse 没有安装 grubby\n    if is_have_cmd_on_disk $os_dir grubby; then\n        # grubby 只处理 GRUB_CMDLINE_LINUX，不会处理 GRUB_CMDLINE_LINUX_DEFAULT\n        # rocky 的 GRUB_CMDLINE_LINUX_DEFAULT 有 crashkernel=auto\n        chroot $os_dir grubby --update-kernel ALL --args selinux=0\n\n        # el7 上面那条 grubby 命令不能设置 /etc/default/grub\n        sed -i 's/selinux=1/selinux=0/' $os_dir/etc/default/grub\n    else\n        # 有可能没有 selinux 参数，但现在的镜像没有这个问题\n        # sed -Ei 's/[[:space:]]?(security|selinux|enforcing)=[^ ]*//g' $os_dir/etc/default/grub\n        sed -i 's/selinux=1/selinux=0/' $os_dir/etc/default/grub\n\n        # 如果需要用 snapshot 可以用 transactional-update grub.cfg\n        chroot $os_dir grub2-mkconfig -o /boot/grub2/grub.cfg\n    fi\n}\n\ndisable_kdump() {\n    os_dir=$1\n\n    # grubby 只处理 GRUB_CMDLINE_LINUX，不会处理 GRUB_CMDLINE_LINUX_DEFAULT\n    # rocky 的 GRUB_CMDLINE_LINUX_DEFAULT 有 crashkernel=auto\n\n    # 新安装的内核依然有 crashkernel，好像是 bug\n    # https://forums.rockylinux.org/t/how-do-i-remove-crashkernel-from-cmdline/13346\n    # 验证过程\n    # yum remove --oldinstallonly   # 删除旧内核\n    # rm -rf /boot/loader/entries/* # 删除启动条目\n    # yum reinstall kernel-core     # 重新安装新内核\n    # cat /boot/loader/entries/*    # 依然有 crashkernel=1G-4G:192M,4G-64G:256M,64G-:512M\n\n    chroot $os_dir grubby --update-kernel ALL --args crashkernel=no\n    # el7 上面那条 grubby 命令不能设置 /etc/default/grub\n    sed -i 's/crashkernel=[^ \"]*/crashkernel=no/' $os_dir/etc/default/grub\n    if chroot $os_dir systemctl -q is-enabled kdump; then\n        chroot $os_dir systemctl disable kdump\n    fi\n}\n\ndownload_qcow() {\n    apk add qemu-img\n    info \"Download qcow2 image\"\n\n    mkdir -p /installer\n    mount /dev/disk/by-label/installer /installer\n\n    qcow_file=/installer/cloud_image.qcow2\n    if [ -n \"$img_type_warp\" ]; then\n        # 边下载边解压，单线程下载\n        # 用官方 wget ，带进度条\n        apk add wget\n        wget $img -O- | pipe_extract >$qcow_file\n    else\n        # 多线程下载\n        download \"$img\" \"$qcow_file\"\n    fi\n}\n\nconnect_qcow() {\n    modprobe nbd nbds_max=1\n    qemu-nbd -c /dev/nbd0 $qcow_file\n\n    # 需要等待一下\n    # https://github.com/canonical/cloud-utils/blob/main/bin/mount-image-callback\n    while ! blkid /dev/nbd0; do\n        echo \"Waiting for qcow file to be mounted...\"\n        sleep 5\n    done\n}\n\ndisconnect_qcow() {\n    if [ -f /sys/block/nbd0/pid ]; then\n        qemu-nbd -d /dev/nbd0\n\n        # 需要等待一下\n        while fuser -sm $qcow_file; do\n            echo \"Waiting for qcow file to be unmounted...\"\n            sleep 5\n        done\n    fi\n}\n\nget_part_size_mb_for_file_size_b() {\n    local file_b=$1\n    local file_mb=$((file_b / 1024 / 1024))\n\n    # ext4 默认参数下\n    #  分区大小   可用大小   利用率\n    #  100 MiB      86 MiB   86.0%\n    #  200 MiB     177 MiB   88.5%\n    #  500 MiB     454 MiB   90.8%\n    #  512 MiB     476 MiB   92.9%\n    # 1024 MiB     957 MiB   93.4%\n    # 2000 MiB    1914 MiB   95.7%\n    # 2048 MiB    1929 MiB   94.1% 这里反而下降了\n    # 5120 MiB    4938 MiB   96.4%\n\n    # 文件系统大约占用 5% 空间\n\n    # 假设 1929M 的文件，计算得到需要创建 2031M 的分区\n    # 但是实测 2048M 的分区才能存放 1929M 的文件\n    # 因此预留不足 150M 时补够 150M\n    local reserve_mb=$((file_mb * 100 / 95 - file_mb))\n    if [ $reserve_mb -lt 150 ]; then\n        reserve_mb=150\n    fi\n\n    part_mb=$((file_mb + reserve_mb))\n    echo \"File size:      $file_mb MiB\" >&2\n    echo \"Part size need: $part_mb MiB\" >&2\n    echo $part_mb\n}\n\nget_cloud_image_part_size() {\n    # 7\n    # https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud-2211.qcow2c 400m\n\n    # 8\n    # https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/AlmaLinux-8-GenericCloud-latest.x86_64.qcow2 600m\n    # https://download.rockylinux.org/pub/rocky/8/images/x86_64/Rocky-8-GenericCloud-Base.latest.x86_64.qcow2 1.8g\n    # https://yum.oracle.com/templates/OracleLinux/OL8/u9/x86_64/OL8U9_x86_64-kvm-b219.qcow2 1g\n    # https://rhel-8.10-x86_64-kvm.qcow2 1g\n\n    # 9\n    # https://cloud.centos.org/centos/9-stream/x86_64/images/CentOS-Stream-GenericCloud-9-latest.x86_64.qcow2 1.2g\n    # https://repo.almalinux.org/almalinux/9/cloud/x86_64/images/AlmaLinux-9-GenericCloud-latest.x86_64.qcow2 600m\n    # https://download.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2 600m\n    # https://yum.oracle.com/templates/OracleLinux/OL9/u3/x86_64/OL9U3_x86_64-kvm-b220.qcow2 600m\n    # https://rhel-9.4-x86_64-kvm.qcow2 900m\n\n    # 10\n    # https://cloud.centos.org/centos/10-stream/x86_64/images/CentOS-Stream-GenericCloud-10-latest.x86_64.qcow2 900m\n\n    # https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/cloud/nocloud_alpine-3.19.1-x86_64-uefi-cloudinit-r0.qcow2 200m\n    # https://kali.download/cloud-images/current/kali-linux-2024.1-cloud-genericcloud-amd64.tar.xz 200m\n    # https://download.opensuse.org/tumbleweed/appliances/openSUSE-Tumbleweed-Minimal-VM.x86_64-Cloud.qcow2 300m\n    # https://download.opensuse.org/distribution/leap/15.5/appliances/openSUSE-Leap-15.5-Minimal-VM.aarch64-Cloud.qcow2 300m\n    # https://mirror.fcix.net/fedora/linux/releases/40/Cloud/x86_64/images/Fedora-Cloud-Base-Generic.x86_64-40-1.14.qcow2 400m\n    # https://geo.mirror.pkgbuild.com/images/latest/Arch-Linux-x86_64-cloudimg.qcow2 500m\n    # https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2 500m\n    # https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img 500m\n    # https://gentoo.osuosl.org/experimental/amd64/openstack/gentoo-openstack-amd64-systemd-latest.qcow2 800m\n\n    # openeuler 是 .qcow2.xz，要解压后才知道 qcow2 大小\n    if [ \"$distro\" = openeuler ]; then\n        # openeuler 20.03 3g\n        if [ \"$releasever\" = 20.03 ]; then\n            echo 3GiB\n        else\n            echo 2GiB\n        fi\n    elif size_bytes=$(get_http_file_size \"$img\"); then\n        # 缩小 btrfs 需要写 qcow2 ，实测写入后只多了 1M，因此不用特殊处理\n        echo \"$(get_part_size_mb_for_file_size_b $size_bytes)MiB\"\n    else\n        # 如果没获取到文件大小\n        echo \"Could not get cloud image size in http response.\" >&2\n        echo 2GiB\n    fi\n}\n\nchroot_dnf() {\n    if is_have_cmd_on_disk /os/ dnf; then\n        chroot /os/ dnf -y \"$@\"\n    else\n        chroot /os/ yum -y \"$@\"\n    fi\n}\n\nchroot_apt_update() {\n    local os_dir=$1\n\n    current_hash=$(cat $os_dir/etc/apt/sources.list $os_dir/etc/apt/sources.list.d/*.sources 2>/dev/null | md5sum)\n    if ! [ \"$saved_hash\" = \"$current_hash\" ]; then\n        chroot $os_dir apt-get update\n        saved_hash=\"$current_hash\"\n    fi\n}\n\nchroot_apt_install() {\n    local os_dir=$1\n    shift\n\n    # 只安装未安装的软件包\n    # 避免更新浪费时间\n    local pkg='' pkgs=''\n    for pkg in \"$@\"; do\n        if chroot $os_dir dpkg -s \"$pkg\" >/dev/null 2>&1; then\n            # 如果已安装则标记为 manual，防止被 autoremove 删除\n            chroot $os_dir apt-mark manual \"$pkg\"\n        else\n            pkgs=\"$pkgs $pkg\"\n        fi\n    done\n\n    # 一次性安装，避免多次 update-initramfs\n    if [ -n \"$pkgs\" ]; then\n        chroot_apt_update $os_dir\n        DEBIAN_FRONTEND=noninteractive chroot $os_dir apt-get install -y $pkgs\n    fi\n}\n\nchroot_apt_remove() {\n    local os_dir=$1\n    shift\n\n    # minimal 镜像 删除 grub-pc 时会安装 grub-efi-amd64\n    # 因此需要先更新索引\n    chroot_apt_update $os_dir\n\n    # 不能用 apt remove --purge -y xxx yyy\n    # 因为如果索引里没有其中一个，会报错，另一个也不会删除\n    local pkgs=\n    for pkg in \"$@\"; do\n        # apt list 会提示 WARNING: apt does not have a stable CLI interface. Use with caution in scripts.\n        # 但又不能用 apt-get list\n        if chroot $os_dir apt list --installed \"$pkg\" | grep -q installed; then\n            pkgs=\"$pkgs $pkg\"\n        fi\n    done\n\n    # 删除 resolvconf 时会弹出建议重启，因此添加 noninteractive\n    DEBIAN_FRONTEND=noninteractive chroot $os_dir apt-get remove --purge --allow-remove-essential -y $pkgs\n}\n\nchroot_apt_autoremove() {\n    local os_dir=$1\n\n    change_confs() {\n        action=$1\n\n        # 只有 16.04 有 01autoremove-kernels\n        # 16.04 结束支持后删除\n        for conf in 01autoremove 01autoremove-kernels; do\n            file=$os_dir/etc/apt/apt.conf.d/$conf\n            case \"$action\" in\n            change)\n                if [ -f $file ]; then\n                    sed -i.orig 's/VersionedKernelPackages/x/; s/NeverAutoRemove/x/' $file\n                fi\n                ;;\n            restore)\n                if [ -f $file.orig ]; then\n                    mv $file.orig $file\n                fi\n                ;;\n            esac\n        done\n    }\n\n    change_confs change\n    DEBIAN_FRONTEND=noninteractive chroot $os_dir apt-get autoremove --purge -y\n    change_confs restore\n}\n\ndel_default_user() {\n    os_dir=$1\n\n    while read -r user; do\n        if grep ^$user':\\$' \"$os_dir/etc/shadow\"; then\n            echo \"Deleting user $user\"\n            chroot \"$os_dir\" userdel -rf \"$user\"\n        fi\n    done < <(grep -v nologin$ \"$os_dir/etc/passwd\" | cut -d: -f1 | grep -v root)\n}\n\nis_el7_family() {\n    is_have_cmd_on_disk \"$1\" yum &&\n        ! is_have_cmd_on_disk \"$1\" dnf\n}\n\ndel_exist_sysconfig_NetworkManager_config() {\n    os_dir=$1\n\n    # 删除云镜像自带的 dhcp 配置，防止歧义\n    rm -rf $os_dir/etc/NetworkManager/system-connections/*.nmconnection\n    rm -rf $os_dir/etc/sysconfig/network-scripts/ifcfg-*\n\n    # 1. 修复 cloud-init 添加了 IPV*_FAILURE_FATAL / may-fail=false\n    #    甲骨文 dhcpv6 获取不到 IP 将视为 fatal，原有的 ipv4 地址也会被删除\n    # 2. 修复 dhcpv6 下，ifcfg 添加了 IPV6_AUTOCONF=no 导致无法获取网关\n    # 3. 修复 dhcpv6 下，NM method=dhcp 导致无法获取网关\n    if false; then\n        ci_file=$os_dir/etc/cloud/cloud.cfg.d/99_fallback.cfg\n\n        insert_into_file $ci_file after '^runcmd:' <<EOF\n  - sed -i '/^IPV[46]_FAILURE_FATAL=/d' /etc/sysconfig/network-scripts/ifcfg-* || true\n  - sed -i '/^may-fail=/d' /etc/NetworkManager/system-connections/*.nmconnection || true\n  - for f in /etc/sysconfig/network-scripts/ifcfg-*; do grep -q '^DHCPV6C=yes' \"\\$f\" && sed -i '/^IPV6_AUTOCONF=no/d' \"\\$f\"; done\n  - sed -i 's/^method=dhcp/method=auto/' /etc/NetworkManager/system-connections/*.nmconnection || true\n  - systemctl is-enabled NetworkManager && systemctl restart NetworkManager || true\nEOF\n    fi\n}\n\ninstall_fnos() {\n    info \"Install fnos\"\n    os_dir=/os\n\n    # 官方安装调用流程\n    # /etc/init.d/run_install.sh > trim-install > trim-grub\n\n    # 挂载 /os\n    mkdir -p /os\n    mount /dev/$xda*2 /os\n\n    # 下载并挂载 iso\n    mkdir -p /os/installer /iso\n    download \"$iso\" /os/installer/fnos.iso\n    mount -o ro /os/installer/fnos.iso /iso\n\n    # 解压 initrd\n    apk add cpio\n    initrd_dir=/os/installer/initrd_dir\n    mkdir -p $initrd_dir\n    (\n        cd $initrd_dir\n        suffix=$(\n            case $(uname -m) in\n            x86_64) echo amd ;;\n            aarch64) echo a64 ;;\n            *) ;;\n            esac\n        )\n        zcat /iso/install.$suffix/initrd.gz | cpio -idm\n    )\n    apk del cpio\n\n    # 获取挂载参数\n    fstab_line_os=$(strings $initrd_dir/trim-install | grep -m1 '^UUID=%s / ')\n    fstab_line_efi=$(strings $initrd_dir/trim-install | grep -m1 '^UUID=%s /boot/efi ')\n    fstab_line_swapfile=$(strings $initrd_dir/trim-install | grep -m1 '^/swapfile none swap ')\n\n    # 删除 initrd\n    rm -rf $initrd_dir\n\n    # 复制 trimfs.tgz 并删除 ISO 以获得更多空间\n    echo \"moving trimfs.tgz...\"\n    cp /iso/trimfs.tgz /os/installer\n    umount /iso\n    rm /os/installer/fnos.iso\n\n    # 挂载 /os/boot/efi\n    if is_efi; then\n        mkdir -p /os/boot/efi\n        mount -o \"$(echo \"$fstab_line_efi\" | awk '{print $4}')\" /dev/$xda*1 /os/boot/efi\n    fi\n\n    # 复制系统\n    info \"Extract fnos\"\n    apk add tar gzip pv\n    pv -f /os/installer/trimfs.tgz | tar zxp --numeric-owner --xattrs-include='*.*' -C /os\n    apk del tar gzip pv\n\n    # 删除 installer (trimfs.tgz)\n    rm -rf /os/installer\n\n    # 挂载 proc sys dev\n    mount_pseudo_fs /os\n\n    # 更改密码\n    if is_need_set_ssh_keys; then\n        set_ssh_keys_and_del_password $os_dir\n    else\n        change_root_password $os_dir\n    fi\n\n    # ssh root 登录，测试用\n    if false; then\n        allow_root_password_login $os_dir\n        chroot $os_dir systemctl enable ssh\n    fi\n\n    # fstab\n    {\n        # /\n        uuid=$(lsblk /dev/$xda*2 -no UUID)\n        echo \"$fstab_line_os\" | sed \"s/%s/$uuid/\"\n\n        # swapfile\n        # 官方安装器即使 swapfile 设为 0 也会有这行\n        echo \"$fstab_line_swapfile\"\n\n        # /boot/efi\n        if is_efi; then\n            uuid=$(lsblk /dev/$xda*1 -no UUID)\n            echo \"$fstab_line_efi\" | sed \"s/%s/$uuid/\"\n        fi\n    } >$os_dir/etc/fstab\n\n    # 更新 initrd，官方安装器也有这一步\n    # 理论上 /var/tmp 要设置 1777 权限，但飞牛官方安装器安装后不是\n    # 需要先创建 /etc/fstab ，否则会有以下警告\n    # W: Couldn't identify type of root file system for fsck hook\n    mkdir -p $os_dir/var/tmp\n    chmod 1777 $os_dir/var/tmp\n    chroot $os_dir update-initramfs -u\n\n    # grub\n    if is_efi; then\n        chroot $os_dir grub-install --efi-directory=/boot/efi\n        chroot $os_dir grub-install --efi-directory=/boot/efi --removable\n    else\n        chroot $os_dir grub-install /dev/$xda\n    fi\n\n    # grub 配置\n    # 取自 strings trim-install | grep GRUB_DISTRIBUTOR\n    sed -i 's/^GRUB_DISTRIBUTOR=.*/GRUB_DISTRIBUTOR=\"FNOS\"/' $os_dir/etc/default/grub\n\n    # grub tty\n    ttys_cmdline=$(get_ttys console=)\n    echo GRUB_CMDLINE_LINUX=\\\"\\$GRUB_CMDLINE_LINUX $ttys_cmdline\\\" >$os_dir/etc/default/grub.d/tty.cfg\n\n    chroot $os_dir update-grub\n\n    # 网卡配置\n    create_cloud_init_network_config /net.cfg\n    create_network_manager_config /net.cfg $os_dir\n    rm /net.cfg\n\n    # 修正网卡名\n    add_fix_eth_name_systemd_service $os_dir\n\n    # frpc\n    add_frpc_systemd_service_if_need $os_dir\n}\n\ninstall_qcow_by_copy() {\n    info \"Install qcow2 by copy\"\n\n    modify_el_ol() {\n        info \"Modify el ol\"\n        os_dir=/os\n\n        # resolv.conf\n        cp_resolv_conf /os\n\n        # 部分镜像有默认配置，例如 centos\n        del_exist_sysconfig_NetworkManager_config /os\n\n        # 删除镜像的默认账户，防止使用默认账户密码登录 ssh\n        del_default_user /os\n\n        # selinux kdump\n        disable_selinux /os\n        disable_kdump /os\n\n        # el7 删除 machine-id 后不会自动重建\n        clear_machine_id /os\n\n        # el7 forks 特殊处理\n        if is_el7_family /os; then\n            # centos 7 eol 换源\n            if [ -f /os/etc/yum.repos.d/CentOS-Base.repo ]; then\n                # 保持默认的 http 因为自带的 ssl 证书可能过期\n                if is_in_china; then\n                    mirror=mirror.nju.edu.cn/centos-vault\n                else\n                    mirror=vault.centos.org\n                fi\n                sed -Ei -e 's,(mirrorlist=),#\\1,' \\\n                    -e \"s,#(baseurl=http://)mirror.centos.org,\\1$mirror,\" /os/etc/yum.repos.d/CentOS-Base.repo\n            fi\n\n            # el7 yum 可能会使用 ipv6，即使没有 ipv6 网络\n            if [ \"$(cat /dev/netconf/*/ipv6_has_internet | sort -u)\" = 0 ]; then\n                echo 'ip_resolve=4' >>/os/etc/yum.conf\n            fi\n\n            # el7 安装 NetworkManager\n            # anolis 7 镜像自带 NetworkManager\n            if ! [ -f /os/usr/lib/systemd/system/NetworkManager.service ]; then\n                chroot_dnf install NetworkManager\n            fi\n            # 服务不存在时会报错\n            chroot /os systemctl disable network 2>/dev/null || true\n            chroot /os systemctl enable NetworkManager\n        fi\n\n        # firmware + microcode\n        if fw_pkgs=$(get_ucode_firmware_pkgs) && [ -n \"$fw_pkgs\" ]; then\n            chroot_dnf install $fw_pkgs\n        fi\n\n        # fstab 删除多余分区\n        # almalinux/rocky 镜像有 boot 分区\n        # oracle 镜像有 swap 分区\n        sed -i '/[[:space:]]\\/boot[[:space:]]/d' /os/etc/fstab\n        sed -i '/[[:space:]]swap[[:space:]]/d' /os/etc/fstab\n\n        # os_part 变量:\n        # mapper/vg_main-lv_root\n        # mapper/opencloudos-root\n\n        # oracle/opencloudos 系统盘从 lvm 改成 uuid 挂载\n        sed -i \"s,/dev/$os_part,UUID=$os_part_uuid,\" /os/etc/fstab\n        if ls /os/boot/loader/entries/*.conf 2>/dev/null; then\n            # 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\n            sed -i \"s,/dev/$os_part,UUID=$os_part_uuid,\" /os/boot/loader/entries/*.conf\n        fi\n\n        # oracle/opencloudos 移除 lvm cmdline\n        chroot /os grubby --update-kernel ALL --remove-args \"resume rd.lvm.lv\"\n        # el7 上面那条 grubby 命令不能设置 /etc/default/grub\n        sed -i 's/rd.lvm.lv=[^ \"]*//g' /os/etc/default/grub\n\n        # fstab 添加 efi 分区\n        if is_efi; then\n            # centos/oracle 要创建efi条目\n            if ! grep /boot/efi /os/etc/fstab; then\n                efi_part_uuid=$(lsblk /dev/$xda*1 -no UUID)\n                echo \"UUID=$efi_part_uuid /boot/efi vfat $efi_mount_opts 0 0\" >>/os/etc/fstab\n            fi\n        else\n            # 删除 efi 条目\n            sed -i '/[[:space:]]\\/boot\\/efi[[:space:]]/d' /os/etc/fstab\n        fi\n\n        remove_grub_conflict_files() {\n            # bios 和 efi 转换前先删除\n\n            # bios转efi出错\n            # centos 和 oracle x86_64 镜像只有 bios 镜像，/boot/grub2/grubenv 是真身\n            # 安装grub-efi时，grubenv 会改成指向efi分区grubenv软连接\n            # 如果安装grub-efi前没有删除原来的grubenv，原来的grubenv将不变，新建的软连接将变成 grubenv.rpmnew\n            # 后续grubenv的改动无法同步到efi分区，会造成grub2-setdefault失效\n\n            # efi转bios出错\n            # 如果是指向efi目录的软连接（例如el8），先删除它，否则 grub2-install 会报错\n            rm -rf /os/boot/grub2/grubenv /os/boot/grub2/grub.cfg\n        }\n\n        # openeuler arm 镜像 grub.cfg 在 /os/grub.cfg，可能给外部的 grub 读取，我们用不到\n        # centos7 有 grub1 的配置\n        rm -rf /os/grub.cfg /os/boot/grub/grub.conf /os/boot/grub/menu.lst\n\n        # 安装引导\n        if is_efi; then\n            # 只有centos 和 oracle x86_64 镜像没有efi，其他系统镜像已经从efi分区复制了文件\n            # openeuler 自带 grub2-efi-ia32，此时安装 grub2-efi 提示已经安装了 grub2-efi-ia32，不会继续安装 grub2-efi-x64\n\n            # 假设极端情况，qcow2 制作时，安装 grub2-efi-x64 时没有挂载 efi 分区，那么 efi 文件会在系统分区下\n            # 但我们复制系统分区时挂载了 /boot/efi，因此 efi 文件会正确地复制到 efi 分区\n            # 因此无需判断 qcow2 的 efi 是否是独立分区\n\n            # rhel 镜像没有源，直接 yum install 安装可能会报错\n            # 因此如果已经安装了要用的包就不再运行 yum install\n            need_install=false\n            need_remove_grub_conflict_files=false\n\n            [ \"$(uname -m)\" = x86_64 ] && arch=x64 || arch=aa64\n            if ! chroot $os_dir rpm -qi grub2-efi-$arch; then\n                need_install=true\n                need_remove_grub_conflict_files=true\n            elif ! chroot $os_dir rpm -qi shim-$arch || ! chroot $os_dir rpm -qi efibootmgr; then\n                need_install=true\n            fi\n\n            if $need_install; then\n                if $need_remove_grub_conflict_files; then\n                    remove_grub_conflict_files\n                fi\n                chroot_dnf install efibootmgr grub2-efi-$arch shim-$arch\n            fi\n            # openeuler arm 25.09 云镜像里面的 grubaa64.efi 是用于 mbr 分区表，$root 是 hd0,msdos1\n            # 因此要重新下载 $root 是 hd0,gpt1 的 grubaa64.efi\n            if $need_reinstall_grub_efi; then\n                chroot_dnf reinstall grub2-efi-$arch\n            fi\n        else\n            # bios\n            remove_grub_conflict_files\n            chroot /os/ grub2-install /dev/$xda\n        fi\n\n        # blscfg 启动项\n        # rocky/almalinux镜像是独立的boot分区，但我们不是\n        # 因此要添加boot目录\n        if ls /os/boot/loader/entries/*.conf 2>/dev/null &&\n            ! grep -q 'initrd /boot/' /os/boot/loader/entries/*.conf; then\n\n            sed -i -E 's,((linux|initrd) /),\\1boot/,g' /os/boot/loader/entries/*.conf\n        fi\n\n        # grub-efi-x64 包里面有 /etc/grub2-efi.cfg\n        # 指向 /boot/efi/EFI/xxx/grub.cfg 或 /boot/grub2/grub.cfg\n        # 指向哪里哪里就是 grub2-mkconfig 应该生成文件的位置\n        # grubby 也是靠 /etc/grub2-efi.cfg 定位 grub.cfg 的位置\n        # openeuler 24.03 x64 aa64 指向的文件不同\n        if is_efi; then\n            grub_o_cfg=$(chroot /os readlink -f /etc/grub2-efi.cfg)\n        else\n            grub_o_cfg=/boot/grub2/grub.cfg\n        fi\n\n        # efi 分区 grub.cfg\n        # https://github.com/rhinstaller/anaconda/blob/346b932a26a19b339e9073c049b08bdef7f166c3/pyanaconda/modules/storage/bootloader/efi.py#L198\n        # https://github.com/rhinstaller/anaconda/commit/15c3b2044367d375db6739e8b8f419ef3e17cae7\n        if is_efi && ! echo \"$grub_o_cfg\" | grep -q '/boot/efi/EFI'; then\n            # oracle linux 文件夹是 redhat\n            # shellcheck disable=SC2010\n            distro_efi=$(cd /os/boot/efi/EFI/ && ls -d -- * | grep -Eiv BOOT)\n            cat <<EOF >/os/boot/efi/EFI/$distro_efi/grub.cfg\nsearch --no-floppy --fs-uuid --set=dev $os_part_uuid\nset prefix=(\\$dev)/boot/grub2\nexport \\$prefix\nconfigfile \\$prefix/grub.cfg\nEOF\n        fi\n\n        # 主 grub.cfg\n        if ls /os/boot/loader/entries/*.conf >/dev/null 2>&1 &&\n            chroot /os/ grub2-mkconfig --help | grep -q update-bls-cmdline; then\n            chroot /os/ grub2-mkconfig -o \"$grub_o_cfg\" --update-bls-cmdline\n        else\n            chroot /os/ grub2-mkconfig -o \"$grub_o_cfg\"\n        fi\n\n        # 网络配置\n        # el7/8 sysconfig\n        # el9 network-manager\n        if [ -f $os_dir/etc/sysconfig/network-scripts/ifup-eth ]; then\n            # sysconfig\n            info 'sysconfig'\n\n            # anolis/openeuler/opencloudos 可能要安装 cloud-init\n            # opencloudos 无法使用 chroot $os_dir command -v xxx\n            # chroot: failed to run command ‘command’: No such file or directory\n            # 注意还要禁用 cloud-init 服务\n            if ! is_have_cmd_on_disk $os_dir cloud-init; then\n                chroot_dnf install cloud-init\n            fi\n\n            # cloud-init 路径\n            # /usr/lib/python2.7/site-packages/cloudinit/net/\n            # /usr/lib/python3/dist-packages/cloudinit/net/\n            # /usr/lib/python3.9/site-packages/cloudinit/net/\n\n            # el7 不认识 static6，但可改成 static，作用相同\n            recognize_static6=true\n            if ls $os_dir/usr/lib/python*/*-packages/cloudinit/net/sysconfig.py 2>/dev/null &&\n                ! grep -q static6 $os_dir/usr/lib/python*/*-packages/cloudinit/net/sysconfig.py; then\n                recognize_static6=false\n            fi\n\n            # cloud-init 20.1 才支持以下配置\n            # https://cloudinit.readthedocs.io/en/20.4/topics/network-config-format-v1.html#subnet-ip\n            # https://cloudinit.readthedocs.io/en/21.1/topics/network-config-format-v1.html#subnet-ip\n            # ipv6_dhcpv6-stateful: Configure this interface with dhcp6\n            # ipv6_dhcpv6-stateless: Configure this interface with SLAAC and DHCP\n            # ipv6_slaac: Configure address with SLAAC\n\n            # el7 最新 cloud-init 版本\n            # centos 7         19.4-7.0.5.el7_9.6  backport 了 ipv6_xxx\n            # openeuler 20.03  19.4-15.oe2003sp4   backport 了 ipv6_xxx\n            # anolis 7         19.1.17-1.0.1.an7   没有更新到 centos7 相同版本,也没 backport ipv6_xxx，坑\n\n            # 最好还修改 ifcfg-eth* 的 IPV6_AUTOCONF\n            # 但实测 anolis7 cloud-init dhcp6 不会生成 IPV6_AUTOCONF，因此暂时不管\n            # https://www.redhat.com/zh/blog/configuring-ipv6-rhel-7-8\n            recognize_ipv6_types=true\n            if ls -d $os_dir/usr/lib/python*/*-packages/cloudinit/net/ 2>/dev/null &&\n                ! grep -qr ipv6_slaac $os_dir/usr/lib/python*/*-packages/cloudinit/net/; then\n                recognize_ipv6_types=false\n            fi\n\n            # 生成 cloud-init 网络配置\n            create_cloud_init_network_config $os_dir/net.cfg \"$recognize_static6\" \"$recognize_ipv6_types\"\n\n            # 转换成目标系统的网络配置\n            chroot $os_dir cloud-init devel net-convert \\\n                -p /net.cfg -k yaml -d out -D rhel -O sysconfig\n            cp $os_dir/out/etc/sysconfig/network-scripts/ifcfg-eth* $os_dir/etc/sysconfig/network-scripts/\n\n            # 清理\n            rm -rf $os_dir/net.cfg $os_dir/out\n\n            # 删除 # Created by cloud-init on instance boot automatically, do not edit.\n            # 修正网络配置问题并显示文件\n            sed -i -e '/^IPV[46]_FAILURE_FATAL=/d' -e '/^#/d' $os_dir/etc/sysconfig/network-scripts/ifcfg-*\n            for file in \"$os_dir/etc/sysconfig/network-scripts/ifcfg-\"*; do\n                if grep -q '^DHCPV6C=yes' \"$file\"; then\n                    sed -i '/^IPV6_AUTOCONF=no/d' \"$file\"\n                fi\n                cat -n \"$file\"\n            done\n        else\n            # Network Manager\n            info 'Network Manager'\n\n            create_cloud_init_network_config /net.cfg\n            create_network_manager_config /net.cfg \"$os_dir\"\n\n            # 清理\n            rm /net.cfg\n        fi\n\n        # 不删除可能网络管理器不会写入dns\n        rm_resolv_conf /os\n    }\n\n    modify_ubuntu() {\n        os_dir=/os\n        info \"Modify Ubuntu\"\n\n        cp_resolv_conf $os_dir\n\n        # 关闭 os prober，因为 os prober 有时很慢\n        cp $os_dir/etc/default/grub $os_dir/etc/default/grub.orig\n        echo 'GRUB_DISABLE_OS_PROBER=true' >>$os_dir/etc/default/grub\n\n        # 更改源\n        if is_in_china; then\n            # 22.04 使用 /etc/apt/sources.list\n            # 24.04 使用 /etc/apt/sources.list.d/ubuntu.sources\n            for file in $os_dir/etc/apt/sources.list $os_dir/etc/apt/sources.list.d/ubuntu.sources; do\n                if [ -f $file ]; then\n                    # cn.archive.ubuntu.com 不在国内还严重丢包\n                    # https://www.itdog.cn/ping/cn.archive.ubuntu.com\n                    sed -i 's/archive.ubuntu.com/mirror.nju.edu.cn/' $file # x64\n                    sed -i 's/ports.ubuntu.com/mirror.nju.edu.cn/' $file   # arm\n                fi\n            done\n        fi\n\n        # 16.04 arm64 镜像没有 grub 引导文件\n        if is_efi && ! [ -d $os_dir/boot/efi/EFI/ubuntu ]; then\n            chroot_apt_install $os_dir efibootmgr shim \"grub-efi-$(get_axx64)\"\n            # 创建 ubuntu 文件夹和 grubaa64.efi\n            DEBIAN_FRONTEND=noninteractive chroot $os_dir dpkg-reconfigure \"grub-efi-$(get_axx64)\"\n\n            cat <<EOF >\"$os_dir/boot/efi/EFI/ubuntu/grub.cfg\"\nsearch.fs_uuid $os_part_uuid root\nset prefix=(\\$root)'/boot/grub'\nconfigfile \\$prefix/grub.cfg\nEOF\n        fi\n\n        # 避免 do-release-upgrade 时自动执行 dpkg-reconfigure grub-xx 但是 efi/biosgrub 分区不存在而导致报错\n        # shellcheck disable=SC2046\n        chroot_apt_remove $os_dir $(is_efi && echo 'grub-pc' || echo 'grub-efi*' 'shim*')\n        chroot_apt_autoremove $os_dir\n\n        # 安装 mbr\n        if ! is_efi; then\n            if false; then\n                # debconf-show grub-pc\n                # 每次开机硬盘名字可能不一样，但是 debian netboot 安装后也是设置了 grub-pc/install_devices\n                echo grub-pc grub-pc/install_devices multiselect /dev/$xda | chroot $os_dir debconf-set-selections # 22.04\n                echo grub-pc grub-pc/cloud_style_installation boolean true | chroot $os_dir debconf-set-selections # 24.04\n                chroot $os_dir dpkg-reconfigure -f noninteractive grub-pc\n            else\n                chroot $os_dir grub-install /dev/$xda\n            fi\n        fi\n\n        # 自带内核：\n        # 常规版本             generic\n        # minimal 20.04/22.04 kvm      # 后台 vnc 无显示\n        # minimal 24.04       virtual\n\n        # debian cloud 内核不支持 ahci，ubuntu virtual 支持\n\n        # 标记所有内核为自动安装\n        # 注意排除 linux-base\n        # 返回值始终为 0\n        pkgs=$(chroot $os_dir apt-mark showmanual \\\n            linux-generic linux-virtual linux-kvm \\\n            linux-image* linux-headers*)\n        chroot $os_dir apt-mark auto $pkgs\n\n        # 安装最佳内核\n        flavor=$(get_ubuntu_kernel_flavor)\n        echo \"Use kernel flavor: $flavor\"\n\n        # 题外话\n        # 如果某个包是 auto 状态且有更新\n        # 则 apt install PKG 只会进行更新，不会将包设置成 manual\n        # 需要再次运行 apt install PKG 才会将包设置成 manual\n\n        # 该方法包含了 apt-mark manual\n        chroot_apt_install $os_dir \"linux-image-$flavor\"\n\n        # 使用 autoremove 删除多余内核\n        chroot_apt_autoremove $os_dir\n\n        # 安装固件+微码\n        if fw_pkgs=$(get_ucode_firmware_pkgs) && [ -n \"$fw_pkgs\" ]; then\n            chroot_apt_install $os_dir $fw_pkgs\n        fi\n\n        # 网络配置\n        # 18.04+ netplan\n        if is_have_cmd_on_disk $os_dir netplan; then\n            # 避免删除 cloud-init 后，minimal 镜像的 netplan.io 被 autoremove\n            chroot $os_dir apt-mark manual netplan.io\n\n            # 生成 cloud-init 网络配置\n            create_cloud_init_network_config $os_dir/net.cfg\n\n            # ubuntu 18.04 cloud-init 版本 23.1.2，因此不用处理 onlink\n\n            # 如果不是输出到 / 则不会生成 50-cloud-init.yaml\n            # 注意比较多了什么东西\n            if false; then\n                chroot $os_dir cloud-init devel net-convert \\\n                    -p /net.cfg -k yaml -d /out -D ubuntu -O netplan\n                sed -Ei \"/^[[:space:]]+set-name:/d\" $os_dir/out/etc/netplan/50-cloud-init.yaml\n                cp $os_dir/out/etc/netplan/50-cloud-init.yaml $os_dir/etc/netplan/\n\n                # 清理\n                rm -rf $os_dir/net.cfg $os_dir/out\n            else\n                chroot $os_dir cloud-init devel net-convert \\\n                    -p /net.cfg -k yaml -d / -D ubuntu -O netplan\n                sed -Ei \"/^[[:space:]]+set-name:/d\" $os_dir/etc/netplan/50-cloud-init.yaml\n\n                # 清理\n                rm -rf $os_dir/net.cfg\n            fi\n        else\n            # 避免删除 cloud-init 后 ifupdown 被 autoremove\n            chroot $os_dir apt-mark manual ifupdown\n\n            # 16.04 镜像用 ifupdown/networking 管理网络\n            # 要安装 resolveconf，不然 /etc/resolv.conf 为空\n            chroot_apt_install $os_dir resolvconf\n            ln -sf /run/resolvconf/resolv.conf $os_dir/etc/resolv.conf.orig\n\n            create_ifupdown_config $os_dir/etc/network/interfaces\n        fi\n\n        # 自带的 60-cloudimg-settings.conf 禁止了 PasswordAuthentication\n        file=$os_dir/etc/ssh/sshd_config.d/60-cloudimg-settings.conf\n        if [ -f $file ]; then\n            sed -i '/^PasswordAuthentication/d' $file\n            if [ -z \"$(cat $file)\" ]; then\n                rm -f $file\n            fi\n        fi\n\n        # 更改 efi 目录的 grub.cfg 写死的 fsuuid\n        # 因为 24.04 fsuuid 对应 boot 分区\n        efi_grub_cfg=$os_dir/boot/efi/EFI/ubuntu/grub.cfg\n        if is_efi; then\n            os_uuid=$(lsblk -rno UUID /dev/$xda*2)\n            sed -Ei \"s|[0-9a-f-]{36}|$os_uuid|i\" $efi_grub_cfg\n\n            # 24.04 移除 boot 分区后，需要添加 /boot 路径\n            if grep \"'/grub'\" $efi_grub_cfg; then\n                sed -i \"s|'/grub'|'/boot/grub'|\" $efi_grub_cfg\n            fi\n        fi\n\n        # 处理 40-force-partuuid.cfg\n        force_partuuid_cfg=$os_dir/etc/default/grub.d/40-force-partuuid.cfg\n        if [ -e $force_partuuid_cfg ]; then\n            if is_virt; then\n                # 更改写死的 partuuid\n                os_part_uuid=$(lsblk -rno PARTUUID /dev/$xda*2)\n                sed -i \"s/^GRUB_FORCE_PARTUUID=.*/GRUB_FORCE_PARTUUID=$os_part_uuid/\" $force_partuuid_cfg\n            else\n                # 独服不应该使用 initrdless boot\n                sed -i \"/^GRUB_FORCE_PARTUUID=/d\" $force_partuuid_cfg\n            fi\n        fi\n\n        # 要重新生成 grub.cfg，因为\n        # 1 我们删除了 boot 分区\n        # 2 改动了 /etc/default/grub.d/40-force-partuuid.cfg\n        chroot $os_dir update-grub\n\n        # 还原 grub 配置（os prober）\n        mv $os_dir/etc/default/grub.orig $os_dir/etc/default/grub\n\n        # fstab\n        # 24.04 镜像有boot分区，但我们不需要\n        sed -i '/[[:space:]]\\/boot[[:space:]]/d' $os_dir/etc/fstab\n        if ! is_efi; then\n            # bios 删除 efi 条目\n            sed -i '/[[:space:]]\\/boot\\/efi[[:space:]]/d' $os_dir/etc/fstab\n        fi\n\n        restore_resolv_conf $os_dir\n    }\n\n    efi_mount_opts=$(\n        case \"$distro\" in\n        ubuntu) echo \"umask=0077\" ;;\n        *) echo \"defaults,uid=0,gid=0,umask=077,shortname=winnt\" ;;\n        esac\n    )\n\n    # yum/apt 安装软件时需要的内存总大小\n    need_ram=$(\n        case \"$distro\" in\n        ubuntu) echo 1024 ;;\n        *) echo 2048 ;;\n        esac\n    )\n\n    connect_qcow\n\n    # 镜像分区格式\n    # centos/rocky/almalinux/rhel: xfs\n    # oracle x86_64:          lvm + xfs\n    # oracle aarch64 cloud:   xfs\n    # alibaba cloud linux 3:  ext4\n\n    is_lvm_image=false\n    if lsblk -f /dev/nbd0p* | grep LVM2_member; then\n        is_lvm_image=true\n        apk add lvm2\n        lvscan\n        vg=$(pvs | grep /dev/nbd0p | awk '{print $2}')\n        lvchange -ay \"$vg\"\n    fi\n\n    mount_nouuid() {\n        part_fstype=\n        for arg in \"$@\"; do\n            case \"$arg\" in\n            /dev/*)\n                part_fstype=$(lsblk -no FSTYPE \"$arg\")\n                break\n                ;;\n            esac\n        done\n\n        case \"$part_fstype\" in\n        xfs) mount -o nouuid \"$@\" ;;\n        *) mount \"$@\" ;;\n        esac\n    }\n\n    # 可以直接选择最后一个分区为系统分区?\n    # almalinux9 boot 分区的类型不是规定的 uuid\n    # openeuler boot 分区是 vfat 格式\n    # openeuler arm 25.09 是 mbr 分区表, efi boot 是同一个分区，vfat 格式\n\n    info \"qcow2 Partitions check\"\n\n    # 检测分区表类型\n    partition_table_format=$(get_partition_table_format /dev/nbd0)\n    need_reinstall_grub_efi=false\n    if is_efi && [ \"$partition_table_format\" = \"msdos\" ]; then\n        need_reinstall_grub_efi=true\n    fi\n\n    # 通过检测文件判断是什么分区\n    os_part='' boot_part='' efi_part=''\n    mkdir -p /nbd-test\n    for part in $(lsblk /dev/nbd0p* --sort SIZE -no NAME,FSTYPE |\n        grep -E ' (ext4|xfs|fat|vfat)$' | awk '{print $1}' | tac); do\n        mapper_part=$part\n        if $is_lvm_image && [ -e /dev/mapper/$part ]; then\n            mapper_part=mapper/$part\n        fi\n\n        if mount_nouuid -o ro /dev/$mapper_part /nbd-test; then\n            if { ls /nbd-test/etc/os-release || ls /nbd-test/*/etc/os-release; } 2>/dev/null; then\n                os_part=$mapper_part\n            fi\n            # shellcheck disable=SC2010\n            # 当 boot 作为独立分区时，vmlinuz 等文件在根目录\n            # 当 boot 不是独立分区时，vmlinuz 等文件在 /boot 目录\n            if ls /nbd-test/ /nbd-test/boot/ 2>/dev/null | grep -Ei '^(vmlinuz|initrd|initramfs)'; then\n                boot_part=$mapper_part\n            fi\n            # mbr + efi 引导 ，分区表没有 esp guid\n            # 因此需要用 efi 文件判断是否 efi 分区\n            # efi 文件可能在 efi 目录的子目录，子目录层数不定\n            if find /nbd-test/ -type f -ipath '/nbd-test/EFI/*.efi' 2>/dev/null | grep .; then\n                efi_part=$mapper_part\n            fi\n            umount /nbd-test\n        fi\n    done\n\n    info \"qcow2 Partitions\"\n    lsblk -f /dev/nbd0 -o +PARTTYPE\n    # 显示 OS/EFI/Boot 文件在哪个分区\n    echo \"---\"\n    echo \"Table:     $partition_table_format\"\n    echo \"Part OS:   $os_part\"\n    echo \"Part EFI:  $efi_part\"\n    echo \"Part Boot: $boot_part\"\n    echo \"---\"\n\n    # 分区寻找方式\n    # 系统/分区          cmdline:root  fstab:efi\n    # rocky             LABEL=rocky   LABEL=EFI\n    # ubuntu            PARTUUID      LABEL=UEFI\n    # 其他el/ol         UUID           UUID\n\n    IFS=, read -r os_part_uuid os_part_label os_part_fstype \\\n        < <(lsblk /dev/$os_part -rno UUID,LABEL,FSTYPE | tr ' ' ,)\n\n    if [ -n \"$efi_part\" ]; then\n        IFS=, read -r efi_part_uuid efi_part_label \\\n            < <(lsblk /dev/$efi_part -rno UUID,LABEL | tr ' ' ,)\n    fi\n\n    mkdir -p /nbd /nbd-boot /nbd-efi\n\n    # 使用目标系统的格式化程序\n    # centos8 如果用alpine格式化xfs，grub2-mkconfig和grub2里面都无法识别xfs分区\n    mount_nouuid /dev/$os_part /nbd/\n    mount_pseudo_fs /nbd/\n    case \"$os_part_fstype\" in\n    ext4) chroot /nbd mkfs.ext4 -F -L \"$os_part_label\" -U \"$os_part_uuid\" /dev/$xda*2 ;;\n    xfs) chroot /nbd mkfs.xfs -f -L \"$os_part_label\" -m uuid=$os_part_uuid /dev/$xda*2 ;;\n    esac\n    umount -R /nbd/\n\n    # TODO: ubuntu 镜像缺少 mkfs.fat/vfat/dosfstools? initrd 不需要检查fs完整性？\n\n    # 创建并挂载 /os\n    mkdir -p /os\n    mount -o noatime /dev/$xda*2 /os/\n\n    # 如果是 efi 则创建 /os/boot/efi\n    # 如果镜像有 efi 分区也创建 /os/boot/efi，用于复制 efi 分区的文件\n    if is_efi || [ -n \"$efi_part\" ]; then\n        mkdir -p /os/boot/efi/\n\n        # 挂载 /os/boot/efi\n        # 预先挂载 /os/boot/efi 因为可能 boot 和 efi 在同一个分区（openeuler 24.03 arm）\n        # 复制 boot 时可以会复制 efi 的文件\n        if is_efi; then\n            mount -o $efi_mount_opts /dev/$xda*1 /os/boot/efi/\n        fi\n    fi\n\n    # 复制系统分区\n    echo Copying os partition...\n    mount_nouuid -o ro /dev/$os_part /nbd/\n    cp -a /nbd/* /os/\n    umount /nbd/\n\n    # 复制独立的boot分区，如果有\n    if [ -n \"$boot_part\" ] && ! [ \"$boot_part\" = \"$os_part\" ]; then\n        echo Copying boot partition...\n        mount_nouuid -o ro /dev/$boot_part /nbd-boot/\n        cp -a /nbd-boot/* /os/boot/\n        umount /nbd-boot/\n    fi\n\n    # 复制独立的efi分区，如果有\n    # 如果 efi 和 boot 是同一个分区，则复制 boot 分区时已经复制了 efi 分区的文件\n    if [ -n \"$efi_part\" ] && ! [ \"$efi_part\" = \"$os_part\" ] && ! [ \"$efi_part\" = \"$boot_part\" ]; then\n        echo Copying efi partition...\n        mount -o ro /dev/$efi_part /nbd-efi/\n        cp -a /nbd-efi/* /os/boot/efi/\n        umount /nbd-efi/\n    fi\n\n    # 断开 qcow 并删除 qemu-img\n    info \"Disconnecting qcow2\"\n    if is_have_cmd vgchange; then\n        vgchange -an\n        apk del lvm2\n    fi\n    disconnect_qcow\n    apk del qemu-img\n\n    # 取消挂载硬盘\n    info \"Unmounting disk\"\n    if is_efi; then\n        umount /os/boot/efi/\n    fi\n    umount /os/\n    umount /installer/\n\n    # 如果镜像有独立的efi分区（包括efi+boot在同一个分区），复制其uuid\n    # 如果有相同uuid的fat分区，则无法挂载\n    # 所以要先复制efi分区，断开nbd再复制uuid\n    # 复制uuid前要取消挂载硬盘 efi 分区\n    if is_efi && [ -n \"$efi_part_uuid\" ] && ! [ \"$efi_part\" = \"$os_part\" ]; then\n        info \"Copy efi partition uuid\"\n        apk add mtools\n        mlabel -N \"$(echo $efi_part_uuid | sed 's/-//')\" -i /dev/$xda*1 ::$efi_part_label\n        apk del mtools\n        update_part\n    fi\n\n    # 删除 installer 分区并扩容\n    info \"Delete installer partition\"\n    apk add parted\n    parted /dev/$xda -s -- rm 3\n    update_part\n    resize_after_install_cloud_image\n\n    # 重新挂载 /os /boot/efi\n    info \"Re-mount disk\"\n    mount -o noatime /dev/$xda*2 /os/\n    if is_efi; then\n        mount -o $efi_mount_opts /dev/$xda*1 /os/boot/efi/\n    fi\n\n    # 创建 swap\n    create_swap_if_ram_less_than $need_ram /os/swapfile\n\n    # 挂载伪文件系统\n    mount_pseudo_fs /os/\n\n    case \"$distro\" in\n    ubuntu) modify_ubuntu ;;\n    *) modify_el_ol ;;\n    esac\n\n    # 基本配置\n    basic_init /os\n\n    # 最后才删除 cloud-init\n    # 因为生成 netplan/sysconfig 网络配置要用目标系统的 cloud-init\n    remove_or_disable_cloud_init /os\n\n    # 删除 swapfile\n    swapoff -a\n    rm -f /os/swapfile\n}\n\nget_partition_table_format() {\n    apk add parted\n    parted \"$1\" -s print | grep 'Partition Table:' | awk '{print $NF}'\n}\n\ndd_qcow() {\n    info \"DD qcow2\"\n\n    if true; then\n        connect_qcow\n\n        partition_table_format=$(get_partition_table_format /dev/nbd0)\n        orig_nbd_virtual_size=$(get_disk_size /dev/nbd0)\n\n        # 检查最后一个分区是否是 btrfs\n        # 即使awk结果为空，返回值也是0，加上 grep . 检查是否结果为空\n        if part_num=$(parted /dev/nbd0 -s print | awk NF | tail -1 | grep btrfs | awk '{print $1}' | grep .); then\n            apk add btrfs-progs\n            mkdir -p /mnt/btrfs\n            mount /dev/nbd0p$part_num /mnt/btrfs\n\n            # 回收空数据块\n            btrfs device usage /mnt/btrfs\n            btrfs balance start -dusage=0 /mnt/btrfs\n            btrfs device usage /mnt/btrfs\n\n            # 计算可以缩小的空间\n            free_bytes=$(btrfs device usage /mnt/btrfs -b | grep Unallocated: | awk '{print $2}')\n            reserve_bytes=$((100 * 1024 * 1024)) # 预留 100M 可用空间\n            skrink_bytes=$((free_bytes - reserve_bytes))\n\n            if [ $skrink_bytes -gt 0 ]; then\n                # 缩小文件系统\n                btrfs filesystem resize -$skrink_bytes /mnt/btrfs\n                # 缩小分区\n                part_start=$(parted /dev/nbd0 -s 'unit b print' | awk \"\\$1==$part_num {print \\$2}\" | sed 's/B//')\n                part_size=$(btrfs filesystem usage /mnt/btrfs -b | grep 'Device size:' | awk '{print $3}')\n                part_end=$((part_start + part_size - 1))\n                umount /mnt/btrfs\n                printf \"yes\" | parted /dev/nbd0 resizepart $part_num ${part_end}B ---pretend-input-tty\n\n                # 缩小 qcow2\n                disconnect_qcow\n                qemu-img resize --shrink $qcow_file $((part_end + 1))\n\n                # 重新连接\n                connect_qcow\n            else\n                umount /mnt/btrfs\n            fi\n        fi\n\n        # 显示分区\n        lsblk -o NAME,SIZE,FSTYPE,LABEL /dev/nbd0\n\n        # 将前1M dd到内存\n        dd if=/dev/nbd0 of=/first-1M bs=1M count=1\n\n        # 将1M之后 dd到硬盘\n        # shellcheck disable=SC2194\n        case 3 in\n        1)\n            # BusyBox dd\n            dd if=/dev/nbd0 of=/dev/$xda bs=1M skip=1 seek=1\n            ;;\n        2)\n            # 用原版 dd status=progress，但没有进度和剩余时间\n            apk add coreutils\n            dd if=/dev/nbd0 of=/dev/$xda bs=1M skip=1 seek=1 status=progress\n            ;;\n        3)\n            # 用 pv\n            apk add pv\n            echo \"Start DD Cloud Image...\"\n            pv -f /dev/nbd0 | dd of=/dev/$xda bs=1M skip=1 seek=1 iflag=fullblock\n            ;;\n        esac\n\n        disconnect_qcow\n    else\n        # 将前1M dd到内存，将1M之后 dd到硬盘\n        qemu-img dd if=$qcow_file of=/first-1M bs=1M count=1\n        qemu-img dd if=$qcow_file of=/dev/disk/by-label/os bs=1M skip=1\n    fi\n\n    # 已 dd 并断开连接 qcow，可删除 qemu-img\n    apk del qemu-img\n\n    # 将前1M从内存 dd 到硬盘\n    umount /installer/\n    dd if=/first-1M of=/dev/$xda\n    rm -f /first-1M\n\n    # gpt 分区表开头记录了备份分区表的位置\n    # 如果 qcow2 虚拟容量 大于 实际硬盘容量\n    # 备份分区表的位置 将超出实际硬盘容量的大小\n    # partprobe 会报错\n    # Error: Invalid argument during seek for read on /dev/vda\n    # parted 也无法正常工作\n    # 需要提前修复分区表\n\n    # 目前只有这个例子，因为其他 qcow2 虚拟容量最多 5g，是设定支持的容量\n    # openSUSE-Leap-15.5-Minimal-VM.x86_64-kvm-and-xen.qcow2 容量是 25g\n    # 缩小 btrfs 分区后 dd 到 10g 的机器上\n    # 备份分区表的位置是 25g\n    # 需要修复到 10g 的位置上\n    # 否则 partprobe parted 都无法正常工作\n\n    # 仅这种情况才用 sgdisk 修复\n    if [ \"$partition_table_format\" = gpt ] &&\n        [ \"$orig_nbd_virtual_size\" -gt \"$(get_disk_size /dev/$xda)\" ]; then\n        fix_gpt_backup_partition_table_by_sgdisk\n    fi\n    update_part\n}\n\nfix_gpt_backup_partition_table_by_sgdisk() {\n    # 当备份分区表超出实际硬盘容量时，只能用 sgdisk 修复分区表\n    # 应用场景：镜像大小超出硬盘实际硬盘，但缩小分区后不超出实际硬盘容量，可以顺利 DD\n    # 例子 openSUSE-Leap-15.5-Minimal-VM.x86_64-kvm-and-xen.qcow2\n\n    # parted 无法修复\n    # parted /dev/$xda -f -s print\n\n    # fdisk/sfdisk 显示主分区表损坏\n    # echo write | sfdisk /dev/$xda\n    # GPT PMBR size mismatch (50331647 != 20971519) will be corrected by write.\n    # The primary GPT table is corrupt, but the backup appears OK, so that will be used.\n\n    # 除此之外的场景应该用 parted 来修复\n\n    apk add sgdisk\n\n    # 两种方法都可以，但都不会修复备份分区表的 GUID\n    # 此时 sgdisk -v /dev/vda 会提示主副分区表 guid 不相同\n    # localhost:~# sgdisk -v /dev/$xda\n    # Problem: main header's disk GUID (A24485F3-2C02-43BD-BF4E-F52E42B00DEA) doesn't\n    # match the backup GPT header's disk GUID (ADAF57BC-B4F5-4E04-BCBA-BDDCD796C388)\n    # You should use the 'b' or 'd' option on the recovery & transformation menu to\n    # select one or the other header.\n    if false; then\n        sgdisk --backup /gpt-partition-table /dev/$xda\n        sgdisk --load-backup /gpt-partition-table /dev/$xda\n    else\n        sgdisk --move-second-header /dev/$xda\n    fi\n\n    # 因此需要运行一次设置 guid\n    if new_guid=$(sgdisk -v /dev/$xda | grep GUID | head -1 | grep -Eo '[0-9A-F-]{36}'); then\n        sgdisk --disk-guid $new_guid /dev/$xda\n    fi\n\n    update_part\n\n    apk del sgdisk\n}\n\n# 适用于 DD 后修复 gpt 备份分区表\nfix_gpt_backup_partition_table_by_parted() {\n    apk add parted\n    parted /dev/$xda -f -s print\n    update_part\n}\n\nresize_after_install_cloud_image() {\n    # 提前扩容\n    # 1 修复 vultr 512m debian 11 generic/genericcloud 首次启动 kernel panic\n    # 2 防止 gentoo 云镜像 websync 时空间不足\n    info \"Resize after dd\"\n    lsblk -f /dev/$xda\n\n    # 打印分区表，并自动修复备份分区表\n    fix_gpt_backup_partition_table_by_parted\n\n    disk_size=$(get_disk_size /dev/$xda)\n    disk_end=$((disk_size - 1))\n\n    # 不能漏掉最后的 _ ，否则第6部分都划到给 last_part_fs\n    IFS=: read -r last_part_num _ last_part_end _ last_part_fs _ \\\n        < <(parted -msf /dev/$xda 'unit b print' | tail -1)\n    last_part_end=$(echo $last_part_end | sed 's/B//')\n\n    if [ $((disk_end - last_part_end)) -ge 0 ]; then\n        printf \"yes\" | parted /dev/$xda resizepart $last_part_num 100% ---pretend-input-tty\n        update_part\n\n        mkdir -p /os\n\n        # lvm ?\n        # 用 cloud-utils-growpart？\n        case \"$last_part_fs\" in\n        ext4)\n            # debian ci\n            apk add e2fsprogs-extra\n            e2fsck -p -f /dev/$xda*$last_part_num\n            resize2fs /dev/$xda*$last_part_num\n            apk del e2fsprogs-extra\n            ;;\n        xfs)\n            # opensuse ci\n            apk add xfsprogs-extra\n            mount /dev/$xda*$last_part_num /os\n            xfs_growfs /dev/$xda*$last_part_num\n            umount /os\n            apk del xfsprogs-extra\n            ;;\n        btrfs)\n            # fedora ci\n            apk add btrfs-progs\n            mount /dev/$xda*$last_part_num /os\n            btrfs filesystem resize max /os\n            umount /os\n            apk del btrfs-progs\n            ;;\n        ntfs)\n            # windows dd\n            apk add ntfs-3g-progs\n            echo y | ntfsresize /dev/$xda*$last_part_num\n            ntfsfix -d /dev/$xda*$last_part_num\n            apk del ntfs-3g-progs\n            ;;\n        esac\n        update_part\n        parted /dev/$xda -s print\n    fi\n}\n\nmount_part_basic_layout() {\n    os_dir=$1\n    efi_dir=$2\n\n    if is_efi || is_xda_gt_2t; then\n        os_part_num=2\n    else\n        os_part_num=1\n    fi\n\n    # 挂载系统分区\n    mkdir -p $os_dir\n    mount -t ext4 /dev/${xda}*${os_part_num} $os_dir\n\n    # 挂载 efi 分区\n    if is_efi; then\n        mkdir -p $efi_dir\n        mount -t vfat -o umask=077 /dev/${xda}*1 $efi_dir\n    fi\n}\n\nmount_part_for_iso_installer() {\n    info \"Mount part for iso installer\"\n\n    if [ \"$distro\" = windows ]; then\n        mount_args=\"-t ntfs3 -o nocase\"\n    else\n        mount_args=\n    fi\n\n    # 挂载主分区\n    mkdir -p /os\n    mount $mount_args /dev/disk/by-label/os /os\n\n    # 挂载其他分区\n    if is_efi; then\n        mkdir -p /os/boot/efi\n        mount /dev/disk/by-label/efi /os/boot/efi\n    fi\n    mkdir -p /os/installer\n    mount $mount_args /dev/disk/by-label/installer /os/installer\n}\n\nget_dns_list_for_win() {\n    if dns_list=$(get_current_dns $1); then\n        i=0\n        for dns in $dns_list; do\n            i=$((i + 1))\n            echo \"set ipv${1}_dns$i=$dns\"\n        done\n    fi\n}\n\ncreate_win_set_netconf_script() {\n    target=$1\n    info \"Create win netconf script\"\n\n    if is_staticv4 || is_staticv6 || is_need_manual_set_dnsv6; then\n        get_netconf_to mac_addr\n        echo \"set mac_addr=$mac_addr\" >$target\n\n        # 生成静态 ipv4 配置\n        if is_staticv4; then\n            get_netconf_to ipv4_addr\n            get_netconf_to ipv4_gateway\n            cat <<EOF >>$target\nset ipv4_addr=$ipv4_addr\nset ipv4_gateway=$ipv4_gateway\n$(get_dns_list_for_win 4)\nEOF\n        fi\n\n        # 生成静态 ipv6 配置\n        if is_staticv6; then\n            get_netconf_to ipv6_addr\n            get_netconf_to ipv6_gateway\n            cat <<EOF >>$target\nset ipv6_addr=$ipv6_addr\nset ipv6_gateway=$ipv6_gateway\nEOF\n        fi\n\n        # 有 ipv6 但需设置 dns 的情况\n        if is_need_manual_set_dnsv6; then\n            cat <<EOF >>$target\n$(get_dns_list_for_win 6)\nEOF\n        fi\n\n        cat -n $target\n    fi\n\n    # 脚本还有关闭ipv6隐私id的功能，所以不能省略\n    # 合并脚本\n    wget $confhome/windows-set-netconf.bat -O- >>$target\n    unix2dos $target\n}\n\ncreate_win_change_rdp_port_script() {\n    target=$1\n    rdp_port=$2\n\n    info \"Create win change rdp port script\"\n\n    echo \"set RdpPort=$rdp_port\" >$target\n    wget $confhome/windows-change-rdp-port.bat -O- >>$target\n    unix2dos $target\n}\n\n# virt-what 要用最新版\n# vultr 1G High Frequency LAX 实际上是 kvm\n# debian 11 virt-what 1.19 显示为 hyperv qemu\n# debian 11 systemd-detect-virt 显示为 microsoft\n# alpine virt-what 1.25 显示为 kvm\n# 所以不要在原系统上判断具体虚拟化环境\n\n# lscpu 也可查看虚拟化环境，但 alpine on lightsail 运行结果为 Microsoft\n# 猜测 lscpu 只参考了 cpuid 没参考 dmi\n# virt-what 可能会输出多行结果，因此用 grep\n\nget_aws_repo() {\n    if is_in_china >&2; then\n        echo https://s3.cn-north-1.amazonaws.com.cn/ec2-windows-drivers-downloads-cn\n    else\n        echo https://s3.amazonaws.com/ec2-windows-drivers-downloads\n    fi\n}\n\nget_client_name_by_build_ver() {\n    build_ver=$1\n\n    if [ \"$build_ver\" -ge 22000 ]; then\n        echo 11\n    elif [ \"$build_ver\" -ge 10240 ]; then\n        echo 10\n    elif [ \"$build_ver\" -ge 9600 ]; then\n        echo 8.1\n    elif [ \"$build_ver\" -ge 9200 ]; then\n        echo 8\n    elif [ \"$build_ver\" -ge 7600 ]; then\n        echo 7\n    elif [ \"$build_ver\" -ge 6000 ]; then\n        echo vista\n    else\n        error_and_exit \"Unknown Build Version: $build_ver\"\n    fi\n}\n\n# 将 AC/SAC 版本号 转换为 LTSC 版本号\n# 用于查找驱动\nget_server_name_by_build_ver() {\n    build_ver=$1\n\n    if [ \"$build_ver\" -ge 26100 ]; then\n        echo 2025\n    elif [ \"$build_ver\" -ge 20348 ]; then\n        echo 2022\n    elif [ \"$build_ver\" -ge 17763 ]; then\n        echo 2019\n    elif [ \"$build_ver\" -ge 14393 ]; then\n        echo 2016\n    elif [ \"$build_ver\" -ge 9600 ]; then\n        echo 2012 r2\n    elif [ \"$build_ver\" -ge 9200 ]; then\n        echo 2012\n    elif [ \"$build_ver\" -ge 7600 ]; then\n        echo 2008 r2\n    elif [ \"$build_ver\" -ge 6001 ]; then\n        echo 2008\n    else\n        error_and_exit \"Unknown Build Version: $build_ver\"\n    fi\n}\n\nis_nt_ver_ge() {\n    local orig sorted\n    orig=$(printf '%s\\n' \"$1\" \"$nt_ver\")\n    sorted=$(echo \"$orig\" | sort -V)\n    [ \"$orig\" = \"$sorted\" ]\n}\n\nget_cloud_vendor() {\n    # busybox blkid 不显示 sr0 的 UUID\n    apk add lsblk\n\n    # http://git.annexia.org/?p=virt-what.git;a=blob;f=virt-what.in;hb=HEAD\n    # virt-what 可识别厂商 aws google_cloud alibaba_cloud alibaba_cloud-ebm\n    if is_dmi_contains \"Amazon EC2\" || is_virt_contains aws; then\n        echo aws\n    elif is_dmi_contains \"Google Compute Engine\" || is_dmi_contains \"GoogleCloud\" || is_virt_contains google_cloud; then\n        echo gcp\n    elif is_dmi_contains \"OracleCloud\"; then\n        echo oracle\n    elif is_dmi_contains \"7783-7084-3265-9085-8269-3286-77\"; then\n        echo azure\n    elif lsblk -o UUID,LABEL | grep -i 9796-932E | grep -iq config-2; then\n        echo ibm\n    elif is_dmi_contains 'Huawei Cloud'; then\n        echo huawei\n    elif is_dmi_contains 'Alibaba Cloud'; then\n        echo aliyun\n    elif is_dmi_contains 'Tencent Cloud'; then\n        echo qcloud\n    fi\n}\n\nget_filesize_mb() {\n    du -m \"$1\" | awk '{print $1}'\n}\n\nis_absolute_path() {\n    # 检查路径是否以/开头\n    # 注意语法和 bash 不同\n    [[ \"$1\" = \"/*\" ]]\n}\n\n# 注意使用方法是 list=$(list_add \"$list\" \"$item_to_add\")\nlist_add() {\n    local list=$1\n    local item_to_add=$2\n    if [ -n \"$list\" ]; then\n        echo \"$list\"\n    fi\n    echo \"$item_to_add\"\n}\n\nis_list_has() {\n    local list=$1\n    local item=$2\n    echo \"$list\" | grep -qFx \"$item\"\n}\n\nget_windows_type_from_windows_drive() {\n    local os_dir=$1\n\n    apk add hivex\n    software_hive=$(find_file_ignore_case $os_dir/Windows/System32/config/SOFTWARE)\n    system_hive=$(find_file_ignore_case $os_dir/Windows/System32/config/SYSTEM)\n    installation_type=$(hivexget $software_hive '\\Microsoft\\Windows NT\\CurrentVersion' InstallationType 2>/dev/null || true)\n    product_type=$(hivexget $system_hive '\\ControlSet001\\Control\\ProductOptions' ProductType 2>/dev/null || true)\n    apk del hivex\n\n    # 根据 win11 multi-session 的情况\n    # InstallationType 比 ProductType 准确\n\n    # Vista wim 和注册表都没有 InstallationType\n    case \"$installation_type\" in\n    Client | Embedded) echo client ;;\n    Server | 'Server Core') echo server ;;\n    *) case \"$product_type\" in\n        WinNT) echo client ;;\n        ServerNT) echo server ;;\n        *) error_and_exit \"Unknown Windows Type\" ;;\n        esac ;;\n    esac\n}\n\nget_windows_arch_from_windows_drive() {\n    local os_dir=$1\n\n    apk add hivex\n    hive=$(find_file_ignore_case $os_dir/Windows/System32/config/SYSTEM)\n    # 没有 CurrentControlSet\n    hivexget $hive 'ControlSet001\\Control\\Session Manager\\Environment' PROCESSOR_ARCHITECTURE\n    apk del hivex\n}\n\ninstall_windows() {\n    get_wim_prop() {\n        wim=$1\n        property=$2\n\n        wiminfo \"$wim\" | grep -i \"^$property:\" | cut -d: -f2- | trim\n    }\n\n    get_image_prop() {\n        wim=$1\n        index=$2\n        property=$3\n\n        wiminfo \"$wim\" \"$index\" | grep -i \"^$property:\" | cut -d: -f2- | trim\n    }\n\n    info \"Process windows iso\"\n    mkdir -p /iso /wim\n\n    # find_file_ignore_case 也在这个文件里面\n    # shellcheck disable=SC1090\n    . <(wget -O- $confhome/windows-driver-utils.sh)\n\n    apk add wimlib\n\n    download $iso /os/windows.iso\n    mount -o ro /os/windows.iso /iso\n\n    sources_boot_wim=$(\n        cd /iso\n        find_file_ignore_case sources/boot.wim 2>/dev/null ||\n            error_and_exit \"can't find boot.wim\"\n    )\n\n    # 一般镜像是 install.wim\n    # en_server_install_disc_windows_home_server_2011_x64_dvd_658487.iso 是 Install.wim\n    # en_windows_vista_sp2_with_update_6003.23713_aio_7in1_x64_v26.01.13_by_adguard.iso 是 swm\n    source_install_wim=$(\n        cd /iso\n        {\n            find_file_ignore_case sources/install.wim ||\n                find_file_ignore_case sources/install.esd ||\n                find_file_ignore_case sources/install.swm\n        } 2>/dev/null || error_and_exit \"can't find install.wim, install.esd or install.swm\"\n    )\n\n    is_swm=false\n    if [[ $(echo \"$source_install_wim\" | to_lower) = '*.swm' ]]; then\n        is_swm=true\n        swm_ref=$(\n            IFS=. read -r name ext < <(basename \"$source_install_wim\")\n            echo \"$name*.$ext\"\n        )\n    fi\n\n    # 防止用了不兼容架构的 iso\n    boot_index=$(get_wim_prop \"/iso/$sources_boot_wim\" 'Boot Index')\n    arch_wim=$(get_image_prop \"/iso/$sources_boot_wim\" \"$boot_index\" 'Architecture' | to_lower)\n    if ! {\n        { [ \"$(uname -m)\" = \"x86_64\" ] && [ \"$arch_wim\" = x86_64 ]; } ||\n            { [ \"$(uname -m)\" = \"x86_64\" ] && [ \"$arch_wim\" = x86 ]; } ||\n            { [ \"$(uname -m)\" = \"aarch64\" ] && [ \"$arch_wim\" = arm64 ]; }\n    }; then\n        error_and_exit \"The machine is $(uname -m), but the iso is $arch_wim.\"\n    fi\n\n    # efi 机器不能安装 32 位 windows\n    if is_efi && [ \"$arch_wim\" = x86 ]; then\n        error_and_exit \"EFI machine can't install 32-bit Windows.\"\n    fi\n\n    iso_install_wim=/iso/$source_install_wim\n    install_wim=/os/installer/$source_install_wim\n\n    # 匹配映像版本\n    # 需要整行匹配，因为要区分 Windows 10 Pro 和 Windows 10 Pro for Workstations\n    image_count=$(wiminfo $iso_install_wim | grep \"^Image Count:\" | cut -d: -f2 | trim)\n    all_image_names=$(wiminfo $iso_install_wim | grep ^Name: | sed 's/^Name: *//')\n    info \"Images Count: $image_count\"\n    echo \"$all_image_names\"\n    echo\n\n    if [ \"$image_count\" = 1 ]; then\n        # 只有一个版本就用那个版本\n        image_name=$all_image_names\n        image_index=1\n    else\n        while true; do\n            # 匹配成功\n            # 改成正确的大小写\n            if matched_image_name=$(printf '%s\\n' \"$all_image_names\" | grep -Fix \"$image_name\"); then\n                image_name=$matched_image_name\n                image_index=$(wiminfo \"$iso_install_wim\" \"$image_name\" | grep 'Index:' | awk '{print $NF}')\n                break\n            fi\n\n            # 匹配失败\n            file=/image-name\n            error \"Invalid image name: $image_name\"\n            echo \"Choose a correct image name by one of follow command in ssh to continue:\"\n            while read -r line; do\n                echo \"  echo '$line' >$file\"\n            done < <(echo \"$all_image_names\")\n\n            # sleep 直到有输入\n            true >$file\n            while ! { [ -s $file ] && image_name=$(cat $file) && [ -n \"$image_name\" ]; }; do\n                sleep 1\n            done\n        done\n    fi\n\n    get_selected_image_prop() {\n        get_image_prop \"$iso_install_wim\" \"$image_index\" \"$1\"\n    }\n\n    # 多会话的信息来自注册表，因为没有官方 iso\n\n    # Installation Type:\n    # https://github.com/search?q=InstallationType+Client+Embedded+Server+Core&type=code\n    # - Client      (普通 windows)\n    # - Server      (windows server 带桌面体验)\n    # - Server Core (windows server 不带桌面体验)\n    # - Embedded    (WES7 / Thin PC)\n    # - Client      (windows 10/11 enterprise 多会话)\n\n    # Product Type:\n    # https://cloud.tencent.com/developer/article/2465206\n    # 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\n    # - WinNT    (普通 windows)\n    # - ServerNT (windows server 带桌面体验)\n    # - ServerNT (windows server 不带桌面体验)\n    # - WinNT    (WES7 / Thin PC)\n    # - ServerNT (windows 10/11 enterprise 多会话)\n\n    # Product Suite:\n    # https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/exinit/productsuite.htm\n    # - Terminal Server (普通 windows)\n    # - Enterprise      (windows server 2025 带桌面体验)\n    # - Enterprise      (windows server 2025 不带桌面体验)\n    # - Terminal Server (windows server 2012 R2 评估板 带桌面体验，注册表也是这个值)\n    # - Terminal Server (windows server 2022 R2 评估板 不带桌面体验，注册表也是这个值)\n    # - Terminal Server (WES7 / Thin PC)\n    # - ?               (windows 10/11 enterprise 多会话)\n\n    # 用内核版本号筛选驱动\n    # 使得可以安装 Hyper-V Server / Azure Stack HCI 等 Windows Server 变种\n    # 7601.24214.180801-1700.win7sp1_ldr_escrow_CLIENT_ULTIMATE_x64FRE_en-us.iso wim 没有 Installation Type\n    # Vista wim 和 注册表 都没有 InstallationType\n    if false; then\n        nt_ver=$(get_selected_image_prop \"Major Version\").$(get_selected_image_prop \"Minor Version\")\n        build_ver=$(get_selected_image_prop \"Build\")\n        installation_type=$(get_selected_image_prop \"Installation Type\")\n    fi\n\n    # 挂载 install.wim，检查\n    # 1. 是否自带 sac 组件\n    # 2. 是否自带 nvme 驱动\n    # 3. 是否支持 sha256\n    # 4. Installation Type\n    # shellcheck disable=SC2046\n    wimmount \"$iso_install_wim\" \"$image_index\" /wim/ \\\n        $($is_swm && echo --ref=$(dirname \"$iso_install_wim\")/$swm_ref)\n\n    # 获取版本号\n    get_windows_version_from_windows_drive /wim\n\n    # 检测 client/server，并转换成标准版 windows 名称\n    windows_type=$(get_windows_type_from_windows_drive /wim)\n    product_ver=$(\n        case \"$windows_type\" in\n        client) get_client_name_by_build_ver \"$build_ver\" ;;\n        server) get_server_name_by_build_ver \"$build_ver\" ;;\n        esac\n    )\n\n    # 检测 sac 和 nvme\n    {\n        find_file_ignore_case /wim/Windows/System32/sacsess.exe && has_sac=true || has_sac=false\n        find_file_ignore_case /wim/Windows/INF/stornvme.inf && has_stornvme=true || has_stornvme=false\n    } >/dev/null 2>&1\n\n    # 检测是否支持 sha256 签名的驱动\n    support_sha256=false\n    if is_nt_ver_ge 6.2; then\n        support_sha256=true\n    else\n        # 安装环境下 drvload.exe 不会验证签名，能安装 sha256 的驱动\n        # 但重启后提示 Windows cannot verify the digital signature for this file.\n\n        # winload.exe/efi 有这串字符\n        # Windows cannot verify the digital signature for this file.\n        # strings -e l winload.exe | grep -i signature\n        # strings -e l winload.efi | grep -i signature\n\n        # 硬盘控制器驱动是 boot-start 驱动，由 winload.exe/efi 验证签名\n        # 网卡驱动不是 boot-start 驱动，由 ci.dll 验证签名\n\n        # win7 sp1 iso 不支持 sha256 的驱动，但是\n        # ci.dll      能找到 8+64 个常量和 oid 0609608648016503040201 0102040365014886600906\n        # winload.exe 能找到 8+64 个常量和 oid 0609608648016503040201 0102040365014886600906\n        # winload.efi 能找到 8+64 个常量和 oid     608648016503040201\n\n        # 官网有提到 KB3033929 和 KB4039648, 应该分别是 2008r2 和 2008 最早支持 sha256 的补丁\n        # https://support.microsoft.com/kb/4472027#:~:text=KB3033929%20%E5%92%8C%20KB4039648\n        # https://support.drweb.cn/sha2\n        # https://support.kaspersky.com/common/compatibility/15761\n        # https://www.internetdownloadmanager.com/register/new_faq/sha256-support-for-outdated-versions-of-Windows.html\n        # https://www.catalog.update.microsoft.com/\n\n        # vista sp2 iso\n        # 用 KB4039648 和 KB4090450 做测试，独立安装时，注册表没有发现另一个 KB 的痕迹\n        # 后续很多补丁如果包含 winload.exe/efi，都是支持 sha256 的新版，因此不能通过检测 KB 编号来判断\n        # HKEY_LOCAL_MACHINE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\Package\n        # HKEY_LOCAL_MACHINE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\PackageDetect\n\n        # vista sp2 iso 独立安装以下补丁时\n        # 补丁          发布日期   BuildLabEx          ubr    winload.exe   winload.efi   ci.dll\n        # KB4039648 旧  2018/2/21  6002.18005(没改变)  没有   6002.24259    6002.24283    6002.24259\n        # KB4039648 新  2018/3/22  6002.18005(没改变)  没有   6002.24259    6002.24298    6002.24259\n        # KB4039648-v2  2018/6/12  6002.24381          没有   6002.24362    6002.24381    6002.24259\n        # KB4474419-v4  2019/10/8  6003.20555          没有   6003.20505    6003.20555    6003.20593\n\n        # win7 sp1 iso 独立安装以下补丁时\n        # KB3033929     2015/3/10  7601.18741          没有   18649/22854  18741/22948    18519/22730\n        # KB4474419-v3  2019/9/10  7601.24384          没有         24149        24384          24158\n\n        # 最早的 KB4039648 KB3033929 都支持 sha256\n        # winload.exe/efi 版本号 >= ci.dll\n        # 因此用 winload.exe/efi 的版本号来判断是否支持 sha256\n\n        apk add pev\n        local maj min build rev\n        winload=$(find_file_ignore_case \"/wim/Windows/System32/winload.$(is_efi && echo efi || echo exe)\")\n        IFS=. read -r maj min build rev \\\n            < <(peres -v \"$winload\" | grep 'Product Version:' | awk '{print $NF}')\n        apk del pev\n\n        # vista/2008\n        # https://support.microsoft.com/kb/KB4039648\n        # https://catalog.update.microsoft.com/Search.aspx?q=KB4039648\n\n        # win7/2008r2 网页有列出文件版本号\n        # https://support.microsoft.com/kb/KB3033929\n        # https://catalog.update.microsoft.com/Search.aspx?q=KB3033929\n\n        # rev 1xxxx 是 GDR 分支\n        # rev 2xxxx 是 LDR 分支\n\n        # vista/2008 版本从 6002 到 6003, rev 减少 4000\n        # https://support.microsoft.com/topic/1335e4d4-c155-52eb-4a45-b85bd1909ca8\n\n        if is_efi; then\n            if { [ \"$maj.$min\" = 6.1 ] && [ \"$build\" -eq 7601 ] && [ \"$rev\" -ge 22948 ]; } ||\n                { [ \"$maj.$min\" = 6.1 ] && [ \"$build\" -eq 7601 ] && [ \"$rev\" -ge 18741 ] && [ \"$rev\" -lt 20000 ]; } ||\n                { [ \"$maj.$min\" = 6.0 ] && [ \"$build\" -eq 6003 ] && [ \"$rev\" -ge 20283 ]; } ||\n                { [ \"$maj.$min\" = 6.0 ] && [ \"$build\" -eq 6002 ] && [ \"$rev\" -ge 24283 ]; }; then\n                support_sha256=true\n            fi\n        else\n            if { [ \"$maj.$min\" = 6.1 ] && [ \"$build\" -eq 7601 ] && [ \"$rev\" -ge 22854 ]; } ||\n                { [ \"$maj.$min\" = 6.1 ] && [ \"$build\" -eq 7601 ] && [ \"$rev\" -ge 18649 ] && [ \"$rev\" -lt 20000 ]; } ||\n                { [ \"$maj.$min\" = 6.0 ] && [ \"$build\" -eq 6003 ] && [ \"$rev\" -ge 20259 ]; } ||\n                { [ \"$maj.$min\" = 6.0 ] && [ \"$build\" -eq 6002 ] && [ \"$rev\" -ge 24259 ]; }; then\n                support_sha256=true\n            fi\n        fi\n    fi\n\n    wimunmount /wim/\n\n    info \"Selected image info\"\n    echo \"Image Name: $image_name\"\n    echo \"Product Version: $product_ver\"\n    echo \"Windows Type: $windows_type\"\n    echo \"NT Version: $nt_ver\"\n    echo \"Build Version: $build_ver\"\n    echo \"Revision Version: $rev_ver\"\n    echo \"-------------------------\"\n    echo \"Has SAC: $has_sac\"\n    echo \"Has StorNVMe: $has_stornvme\"\n    echo \"Support SHA256: $support_sha256\"\n    echo \"-------------------------\"\n    echo\n\n    # 复制 boot.wim 到 /os，用于临时编辑\n    if [ -n \"$boot_wim\" ]; then\n        # 自定义 boot.wim 链接\n        download \"$boot_wim\" /os/boot.wim\n    else\n        cp /iso/$sources_boot_wim /os/boot.wim\n    fi\n\n    # efi 启动目录为 efi 分区\n    # bios 启动目录为 os 分区\n    if is_efi; then\n        boot_dir=/os/boot/efi\n    else\n        boot_dir=/os\n    fi\n\n    # 复制 iso 根目录 boot 开头的文件\n    echo 'Copying boot files...'\n    find /iso -maxdepth 1 -iname 'boot*' -exec cp -r {} \"$boot_dir\" \\;\n\n    # efi 额外复制 iso 根目录 efi 文件夹\n    if is_efi; then\n        echo 'Copying efi files...'\n        find /iso -maxdepth 1 -type d -iname efi -exec cp -r {} \"$boot_dir\" \\;\n    fi\n\n    # 复制iso全部文件(除了boot.wim)到installer分区\n    echo 'Copying installer files...'\n    if false; then\n        # 还需忽略大小写\n        rsync -rv \\\n            --exclude=/sources/boot.wim \\\n            --exclude=/sources/install.wim \\\n            --exclude=/sources/install.esd \\\n            --exclude='/sources/install*.swm' \\\n            /iso/* /os/installer/\n    else\n        (\n            cd /iso\n            find . -type f \\\n                -not -iname boot.wim \\\n                -not -iname install.wim \\\n                -not -iname install.esd \\\n                -not -iname 'install*.swm' \\\n                -exec cp -r --parents {} /os/installer/ \\;\n        )\n    fi\n\n    # 如果是 swm，要先合并成 wim 才能编辑\n    if $is_swm; then\n        install_wim=$(echo \"$install_wim\" | sed 's/\\.swm$/.wim/i')\n        # 防止不格盘二次运行时报错：文件已存在\n        rm -f \"$install_wim\"\n        wimexport --ref=\"$(dirname \"$iso_install_wim\")/$swm_ref\" \"$iso_install_wim\" \"$image_index\" \"$install_wim\"\n        # 只导出了要安装的镜像，因此 image_index 变为 1\n        image_index=1\n    elif false; then\n        # 优化 install.wim\n        # 优点: 可以节省 200M~600M 空间，用来创建虚拟内存\n        #       （意义不大，因为已经删除了 boot.wim 用来创建虚拟内存，vista 除外）\n        # 缺点: 如果 install.wim 只有一个镜像，则只能缩小 10M+\n        time wimexport --threads \"$(get_build_threads 512)\" \"$iso_install_wim\" \"$image_index\" \"$install_wim\"\n        # 只导出了要安装的镜像，因此 image_index 变为 1\n        image_index=1\n        info \"install.wim size\"\n        echo \"Original:  $(get_filesize_mb \"$iso_install_wim\")\"\n        echo \"Optimized: $(get_filesize_mb \"$install_wim\")\"\n        echo\n    else\n        cp \"$iso_install_wim\" \"$install_wim\"\n    fi\n\n    # win11 要求 1GHz 2核（1核超线程也行）\n    # 用注册表无法绕过\n    # https://github.com/pbatard/rufus/issues/1990\n    # https://learn.microsoft.com/windows/iot/iot-enterprise/Hardware/System_Requirements\n    # win11 旧版本安装程序（24h2之前）无法用 setup.exe /product server 跳过 cpu 核数限制，因此在xml里解除限制\n    if [ \"$product_ver\" = \"11\" ] && [ \"$(nproc)\" -le 1 ]; then\n        wiminfo \"$install_wim\" \"$image_index\" --image-property WINDOWS/INSTALLATIONTYPE=Server\n    fi\n\n    # 变量名     使用场景\n    # arch_uname arch命令 / uname -m                      x86_64   aarch64\n    # arch_wim   wiminfo                             x86  x86_64   ARM64\n    # arch       virtio iso / unattend.xml / .inf    x86  amd64    arm64\n    # arch_xdd   virtio msi / xen驱动                x86   x64\n    # arch_dd    华为云驱动                           32    64\n\n    # 将 wim 的 arch 转为驱动和应答文件的 arch\n    case \"$arch_wim\" in\n    x86)\n        arch=x86\n        arch_xdd=x86\n        arch_dd=32\n        ;;\n    x86_64)\n        arch=amd64\n        arch_xdd=x64\n        arch_dd=64\n        ;;\n    arm64)\n        arch=arm64\n        arch_xdd= # xen 没有 arm64 驱动，# virtio 也没有 arm64 msi\n        arch_dd=  # 华为云没有 arm64 驱动\n        ;;\n    esac\n\n    # win7 drvload 可以加载 sha256 签名的驱动\n    # 但系统安装完重启报错 windows cannot verify the digital signature for this file\n    # 需要按 F8 禁用驱动签名\n\n    add_drivers() {\n        info \"Add drivers\"\n\n        drv=/os/drivers\n        mkdir -p \"$drv\" # 驱动下载临时文件夹\n\n        # 这里有坑\n        # $(get_cloud_vendor) 调用了 cache_dmi_and_virt\n        # 但是 $(get_cloud_vendor) 运行在 subshell 里面\n        # subshell 运行结束后里面的变量就消失了\n        # 因此先运行 cache_dmi_and_virt\n        cache_dmi_and_virt\n        vendor=\"$(get_cloud_vendor)\"\n\n        # virtio\n        if is_virt_contains virtio; then\n            if [ \"$vendor\" = aliyun ] && is_nt_ver_ge 6.1 && [ \"$arch_wim\" = x86_64 ]; then\n                add_driver_aliyun_virtio\n            elif [ \"$vendor\" = qcloud ] && is_nt_ver_ge 6.1 && [ \"$arch_wim\" = x86_64 ]; then\n                add_driver_qcloud_virtio\n            # 未测试是否需要专用驱动\n            elif false && [ \"$vendor\" = huawei ] && is_nt_ver_ge 6.0 && { [ \"$arch_wim\" = x86 ] || [ \"$arch_wim\" = x86_64 ]; }; then\n                add_driver_huawei_virtio\n\n            # gcp 官方驱动不全，需要用公版补全\n            # 官方 windows server 模板没有 viorng 设备，但 linux 模板有\n            elif [ \"$vendor\" = gcp ] && is_nt_ver_ge 6.1 && [ \"$arch_wim\" = x86 ] && $support_sha256; then\n                add_driver_gcp_virtio\n                add_driver_generic_virtio \\( -iname viorng.inf -or -iname pvpanic.inf \\)\n\n            elif [ \"$vendor\" = gcp ] && is_nt_ver_ge 6.1 && [ \"$arch_wim\" = x86_64 ] && $support_sha256; then\n                add_driver_gcp_virtio\n                add_driver_generic_virtio -iname viorng.inf\n\n            elif [ \"$vendor\" = gcp ] && [ \"$nt_ver\" = 6.1 ] && [ \"$arch_wim\" = x86_64 ] && ! $support_sha256; then\n                add_driver_gcp_virtio_win6_1_sha1_x64\n                add_driver_generic_virtio \\( -iname viorng.inf -or -iname balloon.inf \\)\n\n            else\n                # 兜底\n                add_driver_generic_virtio\n            fi\n        fi\n\n        # xen\n        if is_virt_contains xen; then\n            # generic_xen 兜底，但未签名，暂停使用\n            if is_nt_ver_ge 6.1 && [ \"$arch_wim\" = x86_64 ]; then\n                add_driver_aws_xen\n            elif is_nt_ver_ge 6.0 && { [ \"$arch_wim\" = x86 ] || [ \"$arch_wim\" = x86_64 ]; }; then\n                add_driver_citrix_xen\n            fi\n        fi\n\n        # vmd\n        # RST v17 不支持 vmd\n        # RST v18 inf 要求 15063 或以上\n        # RST v19 inf 要求 15063 或以上\n        # RST v20 inf 要求 19041 或以上\n        if [ -d /sys/module/vmd ] && [ \"$build_ver\" -ge 15063 ] && [ \"$arch_wim\" = x86_64 ]; then\n            add_driver_vmd\n        fi\n\n        # 厂商驱动\n        case \"$vendor\" in\n        aws)\n            if is_nt_ver_ge 6.1 && { [ \"$arch_wim\" = x86_64 ] || [ \"$arch_wim\" = arm64 ]; }; then\n                add_driver_aws\n            fi\n            ;;\n        azure)\n            # inf 不限版本，未测试\n            if [ \"$arch_wim\" = x86 ] || [ \"$arch_wim\" = x86_64 ]; then\n                add_driver_azure\n            fi\n            ;;\n        gcp)\n            # inf 不限版本，6.0 能装但用不了\n            # x86 x86_64 arm64 都有\n            add_driver_gcp\n            ;;\n        esac\n\n        # intel 网卡驱动\n        # 官网没有提供 vista/2008 驱动\n        # win7 驱动 inf/ndis 不支持 vista/2008\n        if is_nt_ver_ge 6.1 && { [ \"$arch_wim\" = x86 ] || [ \"$arch_wim\" = x86_64 ]; } &&\n            grep -iq 8086 /sys/class/net/e*/device/vendor; then\n            add_driver_intel_nic\n        fi\n\n        # 自定义驱动\n        add_driver_custom\n    }\n\n    add_driver_intel_nic() {\n        info \"Add drivers: Intel NIC\"\n\n        arch_intel=$(\n            case \"$arch_wim\" in\n            x86) echo 32 ;;\n            x86_64) echo x64 ;;\n            esac\n        )\n\n        url=$(\n            case \"$product_ver\" in\n            '7' | '2008 r2')\n                # 现在官网只有 25.0\n                # 25.0 比 24.5 只更新了 ProSet 软件，驱动相同\n                # 25.0 有部分文件是 sha256 签名\n                # 24.3 全部文件是 sha1 签名\n                # 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\n                echo https://downloadmirror.intel.com/18713/eng/prowin${arch_intel}legacy.exe\n                ;;\n            '8' | '8.1')\n                # 之前有 Intel® Network Adapter Driver for Windows 8* - Final Release ，版本 22.7.1\n                # 但已被删除，原因不明\n                # 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\n                # 27.8 有 NDIS63 文件夹，意味着支持 Windows 8\n                # 27.8 相比 22.7.1，可能有些老设备不支持了，但我们不管了\n                echo https://downloadmirror.intel.com/764813/Wired_driver_27.8_${arch_intel}.zip\n                ;;\n            '2012' | '2012 r2')\n                echo https://downloadmirror.intel.com/772074/Wired_driver_28.0_${arch_intel}.zip\n                ;;\n            # 2016 2019 2022 2025 win10 win11\n            *) case \"${arch_intel}\" in\n                32)\n                    echo https://downloadmirror.intel.com/849483/Wired_driver_30.0.1_${arch_intel}.zip\n                    ;;\n                x64)\n                    id=$(\n                        case \"$product_ver\" in\n                        10) echo 18293 ;;\n                        11) echo 727998 ;;\n                        2016) echo 18737 ;;\n                        2019) echo 19372 ;;\n                        2022) echo 706171 ;;\n                        2025) echo 838943 ;;\n                        esac\n                    )\n                    # intel 禁止了 wget 下载网页\n                    wget -U curl/7.54.1 https://www.intel.com/content/www/us/en/download/$id.html -O- |\n                        grep -Eio -m1 \"\\\"https://.+/(Wired_driver|prowin).*${arch_intel}(legacy)?\\.(zip|exe)\\\"\" | tr -d '\"' | grep .\n                    ;;\n                esac ;;\n            esac\n        )\n\n        # 注意 intel 禁止了 aria2 下载\n        download \"$url\" $drv/intel.zip\n\n        # inf 可能是 UTF-16 LE？因此用 rg 搜索\n        # 用 busybox unzip 解压 win10 驱动时，路径和文件名会粘在一起\n        # 但解压 28.0 驱动时，依然会出现这个问题\n        # 因此需要 convert_backslashes\n        apk add unzip ripgrep\n\n        # https://superuser.com/questions/1382839/zip-files-expand-with-backslashes-on-linux-no-subdirectories\n        convert_backslashes() {\n            for file in \"$1\"/*\\\\*; do\n                if [ -f \"$file\" ]; then\n                    target=\"${file//\\\\//}\"\n                    mkdir -p \"${target%/*}\"\n                    mv -v \"$file\" \"$target\"\n                fi\n            done\n        }\n\n        # win7 驱动是 .exe 解压不会报错\n        # win10 驱动是 .zip 解压反而会报错，目测 zip 文件有问题\n        # 在 windows 下解压 win8 的驱动会提示 checksum 错误\n        unzip -o -d $drv/intel/ $drv/intel.zip || true\n        convert_backslashes $drv/intel\n\n        is_have_inf_in_intel_dir() {\n            find $drv/intel -ipath \"*/*.inf\" | grep . >/dev/null\n        }\n\n        # Wired_driver_28.0_x64.zip 需要二次解压\n        if ! is_have_inf_in_intel_dir; then\n            unzip -o -d $drv/intel/ $drv/intel/Wired_driver_*.exe || true\n            convert_backslashes $drv/intel\n        fi\n\n        # 由于上面使用了 || true，因此确认下解压后是否有 inf 文件\n        if ! is_have_inf_in_intel_dir; then\n            error_and_exit \"No .inf file found in intel driver package\"\n        fi\n\n        # Vista RTM 版本号是 6000    NDIS 6.0\n        # 2008  RTM 版本号是 6001    NDIS 6.1\n\n        # 找出驱动文件夹对应的最低系统版本\n        # 1. 驱动可能限制 windows client/server，但我们不区分\n        #    如果装不了也没关系。如果能装但不加载，用户也可以在硬件管理器强制加载驱动\n        # 2. 官网写着 win10 驱动要求 RS5 1809，但是驱动包里有 NDIS65 文件夹，也就是支持 10240\n        # 3. 有可能 NDIS65 文件夹实际要求 NDIS 6.51？但是先不管\n        # https://learn.microsoft.com/en-us/windows-hardware/drivers/network/overview-of-ndis-versions\n        min_support_map=$(cat <<EOF |\n6000  NDIS60\n6001  NDIS61\n7600  NDIS62\n9200  NDIS63\n9600  NDIS64\n10240 NDIS65\n14393 NDIS66\n15063 NDIS67\n16299 NDIS68\n20348 WS2022\n22000 W11\n26100 WS2025\nEOF\n            case \"$windows_type\" in\n            client) grep -E ' (NDIS|W)[0-9]' ;;\n            server) grep -E ' (NDIS|WS)[0-9]' ;;\n            esac)\n\n        for ethx in $(get_eths); do\n            sys_dir=$(get_sys_dir_for_eth $ethx)\n            ven=$(cat $sys_dir/vendor | sed 's/^0x//')\n            dev=$(cat $sys_dir/device | sed 's/^0x//')\n            subsys=$(cat $sys_dir/subsystem_device $sys_dir/subsystem_vendor | sed 's/^0x//' | tr -d '\\n')\n            rev=$(cat $sys_dir/revision | sed 's/^0x//')\n\n            info \"intel nic\"\n            echo \"Ethernet: $ethx\"\n            echo \"Vendor: $ven\"\n            echo \"Device: $dev\"\n            echo \"Subsystem: $subsys\"\n            echo \"Revision: $rev\"\n\n            compatible_ids=\"VEN_$ven&DEV_$dev&SUBSYS_$subsys&REV_$rev\"\n            compatible_ids=\"$compatible_ids|VEN_$ven&DEV_$dev&SUBSYS_$subsys\"\n            compatible_ids=\"$compatible_ids|VEN_$ven&DEV_$dev&REV_$rev\"\n            compatible_ids=\"$compatible_ids|VEN_$ven&DEV_$dev\"\n\n            while read -r min_ver ndis; do\n                if [ \"$build_ver\" -ge \"$min_ver\" ]; then\n                    # 只支持 PE?\n                    # 有   intel\\Release_30.0.zip\\PROXGB\\Win32\\NDIS68\\WinPE\\*.inf\n                    # 没有 intel\\Release_30.0.zip\\PROXGB\\Win32\\NDIS68\\*.inf\n\n                    # find 只要 $drv/intel 存在返回码就是 0\n                    # rg 无需 -E\n                    # 非 WinPE 优先\n                    if infs=$(find $drv/intel -ipath \"*/Win$arch_intel/$ndis/*.inf\" -exec rg -iwl \"$compatible_ids\" {} \\; | grep . ||\n                        find $drv/intel -ipath \"*/Win$arch_intel/$ndis/WinPE/*.inf\" -exec rg -iwl \"$compatible_ids\" {} \\; | grep .); then\n                        for inf in $infs; do\n                            cp_drivers $inf\n                        done\n                        break\n                    fi\n                fi\n            done < <(echo \"$min_support_map\" | tac) # 倒序\n        done\n\n        apk del unzip ripgrep\n    }\n\n    # aws nitro\n    # https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/aws-nvme-drivers.html\n    # https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/enhanced-networking-ena.html\n    add_driver_aws() {\n        info \"Add drivers: AWS\"\n\n        # 未打补丁的 win7 无法使用 sha256 签名的驱动\n        nvme_ver=$(\n            case \"$nt_ver\" in\n            6.1) echo 1.3.2 ;; # sha1 签名\n            6.2 | 6.3) echo 1.5.1 ;;\n            *) echo Latest ;;\n            esac\n        )\n\n        ena_ver=$(\n            case \"$nt_ver\" in\n            6.1) $support_sha256 && echo 2.2.3 || echo 2.1.4 ;;\n            6.2 | 6.3) echo 2.6.0 ;;\n            *) echo Latest ;;\n            esac\n        )\n\n        [ \"$arch_wim\" = arm64 ] && arch_dir=/ARM64 || arch_dir=\n\n        download \"$(get_aws_repo)/NVMe$arch_dir/$nvme_ver/AWSNVMe.zip\" $drv/AWSNVMe.zip\n        download \"$(get_aws_repo)/ENA$arch_dir/$ena_ver/AwsEnaNetworkDriver.zip\" $drv/AwsEnaNetworkDriver.zip\n\n        unzip -o -d $drv/aws/ $drv/AWSNVMe.zip\n        unzip -o -d $drv/aws/ $drv/AwsEnaNetworkDriver.zip\n\n        cp_drivers $drv/aws\n    }\n\n    # citrix xen\n    add_driver_citrix_xen() {\n        info \"Add drivers: Citrix Xen\"\n\n        apk add 7zip\n        download https://s3.amazonaws.com/ec2-downloads-windows/Drivers/Citrix-Win_PV.zip $drv/Citrix-Win_PV.zip\n        unzip -o -d $drv $drv/Citrix-Win_PV.zip\n        case \"$arch_wim\" in\n        x86) override=s ;;    # skip\n        x86_64) override=a ;; # always\n        esac\n        # 排除 $PLUGINSDIR $TEMP\n        exclude='$*'\n        7z x $drv/Citrix_xensetup.exe -o$drv/xen/ -ao$override -x!$exclude\n\n        cp_drivers $drv/xen\n    }\n\n    # aws xen\n    # https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/xen-drivers-overview.html\n    add_driver_aws_xen() {\n        info \"Add drivers: AWS Xen\"\n\n        apk add msitools\n\n        # 8.4.3 的 xenbus 挑创建实例时的初始系统\n        # 初始系统为 windows 的实例支持 8.4.3\n        # 初始系统为 linux 的实例不支持 8.4.3\n\n        # 初始系统为 linux + 安装 8.4.3\n        # 如果用 msi 安装，则不会启用 xenbus，结果是能启动但无法上网\n        # 如果通过 inf 安装，则会启用 xenbus，结果是无法启动\n\n        apk add lscpu\n        hypervisor_vendor=$(lscpu | grep 'Hypervisor vendor:' | awk '{print $3}')\n        apk del lscpu\n\n        aws_pv_ver=$(\n            case \"$nt_ver\" in\n            6.1) $support_sha256 && echo 8.3.5 || echo 8.3.2 ;;\n            6.2 | 6.3)\n                case \"$hypervisor_vendor\" in\n                Microsoft) echo 8.4.3 ;; # 实例初始系统为 Windows，能使用 8.4.3\n                Xen) echo 8.3.5 ;;       # 实例初始系统为 Linux，不能使用 8.4.3\n                esac\n                ;;\n            *) echo Latest ;;\n            esac\n        )\n\n        url=$(\n            case \"$aws_pv_ver\" in\n            8.3.2) echo https://web.archive.org/web/20221016194548/https://s3.amazonaws.com/ec2-windows-drivers-downloads/AWSPV/$aws_pv_ver/AWSPVDriver.zip ;; # win7 sha1\n            *) echo \"$(get_aws_repo)/AWSPV/$aws_pv_ver/AWSPVDriver.zip\" ;;\n            esac\n        )\n\n        download \"$url\" $drv/AWSPVDriver.zip\n\n        unzip -o -d $drv $drv/AWSPVDriver.zip\n        mkdir -p $drv/xen/\n        msiextract $drv/AWSPVDriverSetup.msi -C $drv/xen/\n\n        cp_drivers $drv/xen/.Drivers\n    }\n\n    # citrix xen\n    # https://pvupdates.vmd.citrix.com/updates.json 7.2.0.1555\n    # https://pvupdates.vmd.citrix.com/updates.v9.json 9.3.3.125\n    # https://pvupdates.vmd.citrix.com/autoupdate.v1.json 9.3.3.125\n    # https://pvupdates.vmd.citrix.com/autoupdate.v2.json 9.4.0.146\n    # https://support.citrix.com/s/article/CTX235403-updates-to-xenserver-vm-tools-for-windows-for-xenserver-and-citrix-hypervisor\n\n    # 最高版本\n    # 2012 r2   9.3.1\n    # 2012      9.3.0\n    # 2008 (r2) 7.2.0.1555\n\n    # 9.3.1\n    # https://downloads.xenserver.com/vm-tools-windows/9.3.1/managementagentx64.msi\n    # http://downloadns.citrix.com.edgesuite.net/17461/managementagentx64.msi\n\n    # 7.2.0.1555\n    # http://downloadns.citrix.com.edgesuite.net/14656/managementagentx64.msi\n    # http://downloadns.citrix.com.edgesuite.net/14655/managementagentx86.msi\n\n    # xen\n    # 没签名，暂时用aws的驱动代替\n    # https://lore.kernel.org/xen-devel/E1qKMmq-00035B-SS@xenbits.xenproject.org/\n    # https://xenbits.xenproject.org/pvdrivers/win/\n    # 在 aws t2 上测试，安装 xenbus 会蓝屏，装了其他7个驱动后，能进系统但没网络\n    # 但 aws 应该用aws官方xen驱动，所以测试仅供参考\n    add_driver_generic_xen() {\n        info \"Add drivers: Generic Xen\"\n\n        parts='xenbus xencons xenhid xeniface xennet xenvbd xenvif xenvkbd'\n        mkdir -p $drv/xen/\n        for part in $parts; do\n            download https://xenbits.xenproject.org/pvdrivers/win/$part.tar $drv/$part.tar\n            tar -xf $drv/$part.tar -C $drv/xen/\n        done\n\n        cp_drivers $drv/xen -ipath \"*/$arch_xdd/*\"\n    }\n\n    # virtio\n    # https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/\n    add_driver_generic_virtio() {\n        info \"Add drivers: Generic virtio\"\n\n        # 要区分 win10 / win11 驱动，虽然他们的 NT 版本号都是 10.0，但驱动文件有区别\n        # https://github.com/virtio-win/kvm-guest-drivers-windows/commit/9af43da9e16e2d4bf4ea4663cdc4f29275fff48f\n        # vista >>> 2k8\n        # 10 >>> w10\n        # 2012 r2 >>> 2k12R2\n        virtio_sys=$(\n            case \"$(echo \"$product_ver\" | to_lower)\" in\n            'vista') echo 2k8 ;; # 没有 vista 文件夹\n            *)\n                case \"$windows_type\" in\n                client) echo \"w$product_ver\" ;;\n                server) echo \"$product_ver\" | sed -E -e 's/ //' -e 's/^200?/2k/' -e 's/r2/R2/' ;;\n                esac\n                ;;\n            esac\n        )\n\n        # win7-drivers 分支 win7 文件夹只有一次提交，也就是 173 全家桶\n        # 1. 2020.1.24 https://github.com/virtio-win/virtio-win-pkg-scripts/tree/win7-drivers/data/old-drivers/Win7\n\n        # master 分支 win7 文件夹有 3 次提交，从古到今\n        # https://github.com/virtio-win/virtio-win-pkg-scripts/commits/master/data/old-drivers/Win7\n        # 1. 2020/6/4  sha256，176 全家桶，相当于没发布的 176 iso\n        # 2. 2020/8/10 将部分文件降到 17400，相当于 189~215 iso\n        # 3. 2022/4/14 将部分文件降级，相当于 217~最新版 iso\n\n        # 可改成直接从 github commit 下载 win7 173(sha1) 176(sha256) 全家桶？\n        # 国内可使用 jsdelivr 加速 github\n\n        # 2k12\n        # https://github.com/virtio-win/virtio-win-pkg-scripts/issues/61\n        # 217 ~ 271    2k12 证书有问题，红帽的 virtio-win-1.9.45 没问题\n\n        # win7\n        # https://fedorapeople.org/groups/virt/virtio-win/repo/stable/\n        # https://github.com/virtio-win/virtio-win-pkg-scripts/issues/40\n        # 171-1     sha1   稳定版\n        # 173-9     sha1   对应上面的 win7-drivers 分支，最后一次编译 win7 + sha1，但不是稳定版?\n        # 176       sha256 对应上面的 master-1  最后一次编译 win7，从这次开始是 sha256，此次不提供 iso，编译的文件在之后的 iso 可以找到\n        # 185 ~ 187 sha256 正常工作，win7 文件来自 176\n        # 189 ~ 215 sha1   对应上面的 master-2  气球版本 17400，vultr 死机\n        # 217 ~ 271 sha1   对应上面的 master-3  甲骨文 vioscsi 因硬件 ID 不同用不了，红帽的 virtio-win-1.9.45 也是\n\n        # 甲骨文 vioscsi 硬件 ID 是 PCI\\VEN_1AF4&DEV_1004&SUBSYS_0008108E&REV_00\n        # SUBSYS 的厂商 ID 是甲骨文\n\n        # virtio-win-0.1.173-9\n        # %VirtioScsi.DeviceDesc% = scsi_inst, PCI\\VEN_1AF4&DEV_1004&SUBSYS_00081AF4&REV_00, PCI\\VEN_1AF4&DEV_1004\n        # %VirtioScsi.DeviceDesc% = scsi_inst, PCI\\VEN_1AF4&DEV_1048&SUBSYS_11001AF4&REV_01, PCI\\VEN_1AF4&DEV_1048\n\n        # stable-virtio\n        # %RHELScsi.DeviceDesc% = rhelscsi_inst, PCI\\VEN_1AF4&DEV_1004&SUBSYS_00081AF4&REV_00\n        # %RHELScsi.DeviceDesc% = rhelscsi_inst, PCI\\VEN_1AF4&DEV_1048&SUBSYS_11001AF4&REV_01\n\n        case \"$nt_ver\" in\n        6.0 | 6.1) $support_sha256 &&\n            dir=archive-virtio/virtio-win-0.1.187-1 ||\n            dir=archive-virtio/virtio-win-0.1.173-9 ;;        # vista|w7|2k8|2k8R2\n        6.2 | 6.3) dir=archive-virtio/virtio-win-0.1.215-2 ;; # w8|w8.1|2k12|2k12R2\n        *) dir=stable-virtio ;;\n        esac\n\n        # vista|w7|2k8|2k8R2|arm64 要从 iso 获取驱动\n        if [ \"$nt_ver\" = 6.0 ] || [ \"$nt_ver\" = 6.1 ] || [ \"$arch_wim\" = arm64 ]; then\n            virtio_source=iso\n        else\n            virtio_source=msi\n        fi\n\n        baseurl=https://fedorapeople.org/groups/virt/virtio-win/direct-downloads\n\n        if [ \"$virtio_source\" = iso ]; then\n            download $baseurl/$dir/virtio-win.iso $drv/virtio.iso\n            mkdir -p $drv/virtio\n            mount -o ro $drv/virtio.iso $drv/virtio\n\n            # vista 如果安装气动驱动，会报错 windows could not configure one or more system components\n            # 2008 安装的气球驱动不能用，需要到硬件管理器重新安装设备才能用，无需更新驱动\n            if [ \"$product_ver\" = vista ]; then\n                cp_drivers $drv/virtio -ipath \"*/$virtio_sys/$arch/*\" \"$@\" -not -ipath \"*/balloon/*\"\n            else\n                cp_drivers $drv/virtio -ipath \"*/$virtio_sys/$arch/*\" \"$@\"\n            fi\n        else\n            # coreutils 的 cp mv rm 才有 -v 参数\n            apk add 7zip file coreutils\n            download $baseurl/$dir/virtio-win-gt-$arch_xdd.msi $drv/virtio.msi\n            match=\"FILE_*_${virtio_sys}_${arch}*\"\n            7z x $drv/virtio.msi -o$drv/virtio -i!$match -y -bb1\n\n            # 为没有后缀名的文件添加后缀名\n            (\n                cd $drv/virtio\n                echo \"Recognizing file extension...\"\n                for file in *\"${virtio_sys}_${arch}\"; do\n                    recognized=false\n                    maybe_exts=$(file -b --extension \"$file\")\n\n                    # exe/sys -> sys\n                    # exe/com -> exe\n                    # dll/cpl/tlb/ocx/acm/ax/ime -> dll\n                    for ext in sys exe dll; do\n                        if echo $maybe_exts | grep -qw $ext; then\n                            recognized=true\n                            mv -v \"$file\" \"$file.$ext\"\n                            break\n                        fi\n                    done\n\n                    # 如果识别不了后缀名，就删除此文件\n                    # 因为用不了，免得占用空间\n                    if ! $recognized; then\n                        rm -fv \"$file\"\n                    fi\n                done\n\n                # 将\n                # FILE_netkvm_netkvmco_w8.1_amd64.dll\n                # FILE_netkvm_w8.1_amd64.cat\n                # 改名为\n                # netkvmco.dll\n                # netkvm.cat\n                echo \"Renaming files...\"\n                for file in *; do\n                    new_file=$(echo \"$file\" | sed \"s|FILE_||; s|_${virtio_sys}_${arch}||; s|.*_||\")\n                    mv -v \"$file\" \"$new_file\"\n                done\n            )\n            cp_drivers $drv/virtio \"$@\"\n        fi\n    }\n\n    add_driver_qcloud_virtio() {\n        info \"Add drivers: QCloud virtio\"\n\n        # 测试版?\n        # https://mirrors.tencent.com/install/cts/windows/Drivers.zip\n\n        apk add 7zip\n        download https://mirrors.tencent.com/install/windows/virtio_64_1.0.9.exe $drv/virtio.exe\n        exclude='$*' # 排除 $PLUGINSDIR\n        override=u   # A(u)to rename all\n        7z x $drv/virtio.exe -o$drv/qcloud/ -ao$override -x!$exclude\n\n        # balloon     6.2\n        # balloon_1   6.1\n\n        # netkvm      10.0\n        # netkvm_1    6.1\n        # netkvm_2    6.3\n\n        # viostor     10.0\n        # viostor_1   6.1\n        # viostor_2   6.2\n\n        drivers=$(\n            case \"$nt_ver\" in\n            6.1) echo balloon_1 netkvm_1 viostor_1 ;; # sha1\n            6.2) echo balloon netkvm_1 viostor_2 ;;\n            6.3) echo balloon netkvm_2 viostor_2 ;;\n            *) echo balloon netkvm viostor ;;\n            esac\n        )\n\n        for old_name in $drivers; do\n            part=${old_name%%_*}\n            if ! [ \"$old_name\" = \"$part\" ]; then\n                find $drv/qcloud/$part -type f -iname \"$old_name.*\" | while read -r file; do\n                    ext=\"${file##*.}\"\n                    mv -v \"$file\" \"$drv/qcloud/$part/$part.$ext\"\n                done\n            fi\n            cp_drivers $drv/qcloud/$part/$part.inf\n        done\n    }\n\n    add_driver_huawei_virtio() {\n        info \"Add drivers: Huawei virtio\"\n\n        huawei_sys=$(\n            case \"$(echo \"$product_ver\" | to_lower)\" in\n            vista) echo Vista2008 ;;\n            7) echo 7 ;;\n            8) [ \"$arch_wim\" = x86 ] && echo 7 || echo 2012 ;;      # 没有 win8 32/64\n            8.1) [ \"$arch_wim\" = x86 ] && echo 7 || echo 2012_R2 ;; # 没有 win8.1 32/64\n            10 | 11) echo 10 ;;\n            2008) echo Vista2008 ;;\n            '2008 r2') echo 2008_R2 ;;\n            2012) [ \"$arch_wim\" = x86 ] && echo 2008_R2 || echo 2012 ;; # 没有 2012 32\n            '2012 r2') echo 2012_R2 ;;\n            2016 | 2019 | 202*) echo 2016 ;;\n            esac\n        )\n\n        download https://ecs-instance-driver.obs.cn-north-1.myhuaweicloud.com/vmtools-windows.zip $drv/vmtools-windows.zip\n        unzip -o -d $drv $drv/vmtools-windows.zip\n        mkdir -p $drv/huawei\n        mount -o ro $drv/vmtools-windows.iso $drv/huawei\n\n        cp_drivers $drv/huawei -ipath \"*/upgrade/windows ${huawei_sys}_${arch_dd}/drivers/*\"\n    }\n\n    add_driver_aliyun_virtio() {\n        info \"Add drivers: Aliyun virtio\"\n\n        aliyun_sys=$(\n            case \"$nt_ver\" in\n            6.1) echo 2008R2 ;;\n            6.2 | 6.3) echo 2012R2 ;; # 实际上是 2012 的驱动\n            *) echo 2016 ;;\n            esac\n        )\n\n        subdir=\n        if [ \"$nt_ver\" = 6.1 ] && ! $support_sha256; then\n            subdir=58017/ # sha1\n        fi\n\n        region=cn-hangzhou\n\n        download https://windows-driver-$region.oss-$region.aliyuncs.com/virtio/${subdir}AliyunVirtio_WIN$aliyun_sys.zip \\\n            $drv/AliyunVirtio.zip\n        unzip -o -d $drv $drv/AliyunVirtio.zip\n\n        apk add innoextract\n        innoextract -d $drv/aliyun/ $drv/AliyunVirtio_*_WIN${aliyun_sys}_$arch_xdd.exe\n        apk del innoextract\n\n        cp_drivers $drv/aliyun -ipath \"*/C$/Program Files/AliyunVirtio/*/drivers/*\"\n    }\n\n    # gcp virtio win7 x64 sha1\n    # 缺 balloon viorng\n    add_driver_gcp_virtio_win6_1_sha1_x64() {\n        info \"Add drivers: GCP virtio win6.1 sha1 x64\"\n\n        # 用到 nvme 时才下载 nvme 驱动\n        # 因为 win7 可以通过更新获得 nvme 驱动\n        # 而且谷歌推荐使用微软 nvme 驱动\n        # (google-compute-engine-driver-nvme 2.0.0 更新内容是删除谷歌 nvme 驱动)\n        mkdir -p $drv/gce/win6.1sha1\n        for file in \\\n            WdfCoInstaller01009.dll WdfCoInstaller01011.dll \\\n            netkvm.inf netkvm.cat netkvm.sys netkvmco.dll \\\n            pvpanic.inf pvpanic.sys pvpanic.cat \\\n            vioscsi.inf vioscsi.sys vioscsi.cat \\\n            $([ -d /sys/module/nvme ] && ! $has_stornvme && echo nvme.inf nvme64.cat nvme.sys); do\n            download https://storage.googleapis.com/gce-windows-drivers-public/win6.1sha1/$file $drv/gce/win6.1sha1/$file\n        done\n        cp_drivers $drv/gce/win6.1sha1\n    }\n\n    # gcp virtio win7+ sha256\n    # x86 缺 viorng pvpanic\n    # x64 缺 viorng\n    # https://github.com/GoogleCloudPlatform/compute-image-tools/tree/master/daisy_workflows/image_build/windows\n    # 官方是从 https://console.cloud.google.com/storage/browser/gce-windows-drivers-public 下载驱动，安装系统后再 googet 更新驱动\n    # 我们一步到位从 googet 下载驱动\n    add_driver_gcp_virtio() {\n        info \"Add drivers: GCP virtio\"\n\n        mkdir -p $drv/gce\n        gce_repo=https://packages.cloud.google.com/yuck\n        download $gce_repo/repos/google-compute-engine-stable/index $drv/gce/gce.json\n        for part in balloon netkvm pvpanic vioscsi; do\n            # gcp 提供的 pvpanic 没有 x86 驱动\n            if [ \"$part\" = pvpanic ] && [ \"$arch_wim\" = x86 ]; then\n                continue\n            fi\n\n            mkdir -p $drv/gce/$part\n            link=$(grep -o \"/pool/.*-google-compute-engine-driver-$part.*\\.goo\" $drv/gce/gce.json)\n            wget $gce_repo$link -O- | tar xz -C $drv/gce/$part\n\n            [ \"$arch_wim\" = x86 ] && suffix=-32 || suffix=\n            cp_drivers $drv/gce/$part -ipath \"*/win$nt_ver$suffix/*\"\n        done\n    }\n\n    # gcp\n    # x86 x86_64 arm64 都有\n    # win7 驱动是 sha256 签名\n    add_driver_gcp() {\n        info \"Add drivers: GCP\"\n\n        # https://packages.cloud.google.com/yuck/repos/google-compute-engine-stable/index\n        # https://packages.cloud.google.com/yuck/repos/google-compute-engine-driver-gvnic-gq-stable/index\n        # 官方镜像的 gvnic 是从 gvnic-gq-stable 获取的，版本低一点，但更稳定?\n\n        mkdir -p $drv/gce\n        gce_repo=https://packages.cloud.google.com/yuck\n        download $gce_repo/repos/google-compute-engine-stable/index $drv/gce/gce.json\n        for part in gvnic gga; do\n            # gvnic 没有 arm64\n            if [ \"$part\" = gvnic ] && [ \"$arch_wim\" = arm64 ]; then\n                continue\n            fi\n\n            mkdir -p $drv/gce/$part\n            link=$(grep -o \"/pool/.*-google-compute-engine-driver-$part.*\\.goo\" $drv/gce/gce.json)\n            wget $gce_repo$link -O- | tar xz -C $drv/gce/$part\n\n            # inf 不限版本\n            # 但 win7 gvnic ndis 版本是 6.2，vista/2008 能装但用不了\n            # https://github.com/GoogleCloudPlatform/compute-virtual-ethernet-windows/blob/cad1edf7a05465f4972a81f2c015952fd228b5e3/src/gvnic.vcxproj#L298\n            if false; then\n                for suffix in '' '-32'; do\n                    if [ -d \"$drv/gce/$part/win6.1$suffix\" ]; then\n                        cp -r \"$drv/gce/$part/win6.1$suffix\" \"$drv/gce/$part/win6.0$suffix\"\n                    fi\n                done\n            fi\n\n            case \"$part\" in\n            gvnic)\n                [ \"$arch_wim\" = x86 ] && suffix=-32 || suffix=\n                cp_drivers $drv/gce/gvnic -ipath \"*/win$nt_ver$suffix/*\"\n                ;;\n            gga)\n                cp_drivers $drv/gce/gga -ipath \"*/win$nt_ver/*\"\n                ;;\n            esac\n        done\n    }\n\n    # azure\n    # https://learn.microsoft.com/azure/virtual-network/accelerated-networking-mana-windows\n    add_driver_azure() {\n        info \"Add drivers: Azure\"\n\n        download https://aka.ms/manawindowsdrivers $drv/azure.zip\n        unzip $drv/azure.zip -d $drv/azure/\n        cp_drivers $drv/azure\n    }\n\n    add_driver_vmd() {\n        # RST v20 不支持 11代 PCI\\VEN_8086&DEV_9A0B\n        support_v19=false\n        support_v20=false\n        for d in /sys/bus/pci/devices/*; do\n            vendor=$(cat \"$d/vendor\" 2>/dev/null)\n            device=$(cat \"$d/device\" 2>/dev/null)\n            if [ \"$vendor\" = \"0x8086\" ]; then\n                case \"$device\" in\n                \"0x9a0b\") support_v19=true && support_v20=false && break ;;\n                \"0x467f\") support_v19=true && support_v20=true && break ;;\n                \"0xa77f\") support_v19=true && support_v20=true && break ;;\n                \"0x7d0b\") support_v19=false && support_v20=true && break ;;\n                \"0xad0b\") support_v19=false && support_v20=true && break ;;\n                esac\n            fi\n        done\n\n        local page=\n        if $support_v20 && [ \"$build_ver\" -ge 19041 ]; then\n            page=https://www.intel.com/content/www/us/en/download/849936.html\n        elif $support_v19 && [ \"$build_ver\" -ge 15063 ]; then\n            page=https://www.intel.com/content/www/us/en/download/849933.html\n        fi\n\n        if [ -n \"$page\" ]; then\n            # intel 禁止了 wget 下载网页\n            local url\n            url=$(wget -U curl/7.54.1 \"$page\" -O- |\n                grep -Eio -m1 \"\\\"https://.+/SetupRST\\.exe\\\"\" | tr -d '\"' | grep .)\n\n            # 注意 intel 禁止了 aria2 下载\n            download $url $drv/SetupRST.exe\n            apk add 7zip\n            7z x $drv/SetupRST.exe -o$drv/SetupRST -i!.text\n            7z x $drv/SetupRST/.text -o$drv/vmd\n            apk del 7zip\n            cp_drivers $drv/vmd\n        else\n            # 如果开启了 vmd 但硬盘不在 vmd 上，linux 会自动加载 vmd 模块?\n            # 还要判断主硬盘是否在 vmd 上，如果不在，即使没有 vmd 驱动也可继续安装\n            # 因此目前先不中止脚本\n            : error_and_exit \"can't find suitable vmd driver\"\n        fi\n    }\n\n    # 脚本自动检测驱动可能有问题\n    # 假设是 win7 时代的网卡，官网没有 win10 驱动，系统也不自带\n    # 但实际上 win10 可以用 win7 的驱动\n    # 这种情况即使脚本自动下载 win10 的驱动包，也不会包含这个驱动\n    # 应该下载 win7 的驱动\n    # 因此只能交给用户自己添加驱动\n\n    add_driver_custom() {\n        if [ -d /custom_drivers/ ]; then\n            cp_drivers custom /custom_drivers/\n            # 复制后不删除，因为脚本可能再次运行\n        fi\n    }\n\n    # 修改应答文件\n    download $confhome/windows.xml /tmp/autounattend.xml\n    locale=$(get_selected_image_prop 'Default Language')\n    use_default_rdp_port=$(is_need_change_rdp_port && echo false || echo true)\n    password_base64=$(get_password_windows_administrator_base64)\n    # 7601.24214.180801-1700.win7sp1_ldr_escrow_CLIENT_ULTIMATE_x64FRE_en-us.iso Image Name 为空\n    # 将 xml Image Name 的值设为空可以正常安装\n    sed -i \\\n        -e \"s|%arch%|$arch|\" \\\n        -e \"s|%image_name%|$image_name|\" \\\n        -e \"s|%locale%|$locale|\" \\\n        -e \"s|%administrator_password%|$password_base64|\" \\\n        -e \"s|%use_default_rdp_port%|$use_default_rdp_port|\" \\\n        /tmp/autounattend.xml\n\n    # 修改应答文件，分区配置\n    if is_efi; then\n        sed -i \"s|%installto_partitionid%|3|\" /tmp/autounattend.xml\n    else\n        sed -i \"s|%installto_partitionid%|1|\" /tmp/autounattend.xml\n    fi\n\n    # vista/2008 有这行安装会报错\n    if [ \"$nt_ver\" = 6.0 ]; then\n        sed -i \"/EnableFirewall/d\" /tmp/autounattend.xml\n    fi\n\n    # 2012 r2，删除 key 字段，报错 Windows cannot read the <ProductKey> setting from the unattend answer file，即使创建 ei.cfg\n    # ltsc 2021，有 ei.cfg，填空白 key 正常\n    # ltsc 2021 n，有 ei.cfg，填空白 key 报错 Windows Cannot find Microsoft software license terms\n    # 评估版 iso ei.cfg 有 EVAL 字样，填空白 key 报错 Windows Cannot find Microsoft software license terms\n\n    # key\n    if [ \"$product_ver\" = vista ]; then\n        # vista 无人值守安装需要密钥，密钥可与 edition 不一致\n        # https://learn.microsoft.com/en-us/windows-server/get-started/kms-client-activation-keys\n        # 从镜像获取默认密钥\n        setup_cfg=$(get_path_in_correct_case /os/installer/sources/inf/setup.cfg)\n        key=$(del_cr <\"$setup_cfg\" | grep -Eix 'Value=([A-Z0-9]{5}-){4}[A-Z0-9]{5}' | cut -d= -f2 | grep .)\n        sed -i \"s/%key%/$key/\" /tmp/autounattend.xml\n    else\n        if [ -f \"$(get_path_in_correct_case /os/installer/sources/ei.cfg)\" ]; then\n            # 镜像有 ei.cfg，删除 key 字段\n            sed -i \"/%key%/d\" /tmp/autounattend.xml\n        else\n            # 镜像无 ei.cfg，填空白 key\n            sed -i \"s/%key%//\" /tmp/autounattend.xml\n        fi\n    fi\n\n    # 挂载 boot.wim\n    info \"mount boot.wim\"\n    wimmountrw /os/boot.wim \"$boot_index\" /wim/\n\n    # 防止重复\n    copyed_infs=\n    cp_drivers() {\n        if [ \"$1\" = custom ]; then\n            shift\n            dst=$(get_path_in_correct_case \"/wim/custom_drivers\")\n        else\n            dst=$(get_path_in_correct_case \"/wim/drivers\")\n        fi\n\n        src=$1\n        shift\n\n        # -not -iname \"*.pdb\" \\\n        # -not -iname \"dpinst.exe\" \\\n\n        # 这里需要在 while 里面变更 $copyed_infs，因此不能用 find | while\n        while read -r inf; do\n            if ! is_list_has \"$copyed_infs\" \"$inf\"; then\n                parse_inf_and_cp_driever \"$inf\" \"$dst\" \"$arch\" false\n                copyed_infs=$(list_add \"$copyed_infs\" \"$inf\")\n            fi\n        done < <(find $src -type f -iname \"*.inf\" \"$@\")\n    }\n\n    # 添加驱动\n    add_drivers\n\n    # win7 要添加 bootx64.efi 到 efi 目录\n    if is_efi; then\n        [ $arch = amd64 ] && boot_efi=bootx64.efi || boot_efi=bootaa64.efi\n\n        local src dst\n        dst=$(get_path_in_correct_case /os/boot/efi/EFI/boot/$boot_efi)\n        if ! [ -f $dst ]; then\n            mkdir -p \"$(dirname $dst)\"\n            src=$(get_path_in_correct_case /wim/Windows/Boot/EFI/bootmgfw.efi)\n            cp \"$src\" \"$dst\"\n        fi\n    fi\n\n    # 复制应答文件\n    # 移除注释，否则 windows-setup.bat 重新生成的 autounattend.xml 有问题\n    wim_autounattend_xml=$(get_path_in_correct_case /wim/autounattend.xml)\n    wim_windows_xml=$(get_path_in_correct_case /wim/windows.xml)\n    wim_setup_exe=$(get_path_in_correct_case /wim/setup.exe)\n\n    apk add xmlstarlet\n    xmlstarlet ed -d '//comment()' /tmp/autounattend.xml >$wim_autounattend_xml\n    unix2dos $wim_autounattend_xml\n    info \"autounattend.xml\"\n    # 查看最终文件，并屏蔽密码\n    xmlstarlet ed -d '//*[name()=\"AdministratorPassword\" or name()=\"Password\"]' $wim_autounattend_xml | cat -n\n    apk del xmlstarlet\n\n    # 避免无参数运行 setup.exe 时自动安装\n    mv $wim_autounattend_xml $wim_windows_xml\n\n    # 复制安装脚本\n    # https://slightlyovercomplicated.com/2016/11/07/windows-pe-startup-sequence-explained/\n    # https://learn.microsoft.com/previous-versions/windows/it-pro/windows-vista/cc721977(v=ws.10)\n    mv $wim_setup_exe $wim_setup_exe.disabled\n\n    # 如果有重复的 Windows/System32 文件夹，会提示找不到 winload.exe 无法引导\n    # win7 win10  boot.wim 是 Windows/System32，install.wim 是 Windows/System32\n    # win2016     boot.wim 是 windows/system32，install.wim 是 Windows/System32\n    # wimmount 无法挂载成忽略大小写\n\n    startnet_cmd=$(get_path_in_correct_case /wim/Windows/System32/startnet.cmd)\n    winpeshl_ini=$(get_path_in_correct_case /wim/Windows/System32/winpeshl.ini)\n\n    download $confhome/windows-setup.bat $startnet_cmd\n    # dism 手动释放镜像时用\n    # sed -i \"s|@image_name@|$image_name|\" \"$startnet.cmd\"\n\n    # shellcheck disable=SC2154\n    if [ \"$force_old_windows_setup\" = 1 ]; then\n        sed -i 's/ForceOldSetup=0/ForceOldSetup=1/i' $startnet_cmd\n    fi\n\n    # 有 SAC 组件时，启用 EMS\n    if $has_sac; then\n        sed -i 's/EnableEMS=0/EnableEMS=1/i' $startnet_cmd\n    fi\n\n    # 4kn EFI 分区最少要 260M\n    # https://learn.microsoft.com/windows-hardware/manufacture/desktop/hard-drives-and-partitions\n    if is_4kn; then\n        sed -i 's/is4kn=0/is4kn=1/i' $startnet_cmd\n    fi\n\n    # Windows Thin PC 有 Windows\\System32\\winpeshl.ini\n    # [LaunchApps]\n    # %SYSTEMDRIVE%\\windows\\system32\\drvload.exe, %SYSTEMDRIVE%\\windows\\inf\\sdbus.inf\n    # %SYSTEMDRIVE%\\setup.exe\n    if [ -f \"$winpeshl_ini\" ]; then\n        info \"mod winpeshl.ini\"\n        # https://learn.microsoft.com/previous-versions/windows/it-pro/windows-vista/cc721977(v=ws.10)\n        # 两种方法都可以，第一种是原版命令\n        sed -i 's|setup.exe|windows\\\\system32\\\\cmd.exe, \"/k %SYSTEMROOT%\\\\system32\\\\startnet.cmd\"|i' \"$winpeshl_ini\"\n        # sed -i 's|setup.exe|windows\\\\system32\\\\startnet.cmd|i' \"$winpeshl_ini\"\n        cat -n \"$winpeshl_ini\"\n    fi\n\n    # 提交修改 boot.wim\n    info \"Unmount boot.wim\"\n    wimunmount --commit /wim/\n\n    # 原地优化可以用以下命令之一\n    # wimdelete /os/boot.wim 1\n    # wimoptimize /os/boot.wim\n\n    # 优化 boot.wim 并复制到正确的位置\n    if is_nt_ver_ge 6.1; then\n        # win7 或以上删除 boot.wim 镜像 1 不会报错\n        # 因为 win7 winre 镜像在 install.wim Windows\\System32\\Recovery\\winRE.wim\n        images=$boot_index\n    else\n        # vista 删除 boot.wim 镜像 1 会报错\n        # Windows cannot access the required file Drive:\\Sources\\Boot.wim.\n        # Make sure all files required for installation are available and restart the installation.\n        # Error code: 0x80070491\n        # vista install.wim 没有 Windows\\System32\\Recovery\\winRE.wim\n        images=all\n    fi\n    mkdir -p \"$(get_path_in_correct_case \"$(dirname $boot_dir/$sources_boot_wim)\")\"\n    # 防止不格盘二次运行时报错：文件已存在\n    rm -f $boot_dir/$sources_boot_wim\n    wimexport --boot /os/boot.wim \"$images\" $boot_dir/$sources_boot_wim\n    info \"boot.wim size\"\n    echo \"Original:      $(get_filesize_mb /iso/$sources_boot_wim)\"\n    echo \"Added Drivers: $(get_filesize_mb /os/boot.wim)\"\n    echo \"Optimized:     $(get_filesize_mb \"$boot_dir/$sources_boot_wim\")\"\n    echo\n\n    # vista 安装时需要 boot.wim，原因见上面\n    if [ \"$nt_ver\" = 6.0 ] &&\n        ! [ -e /os/installer/$sources_boot_wim ]; then\n        cp $boot_dir/$sources_boot_wim /os/installer/$sources_boot_wim\n    fi\n\n    # windows 7 没有 invoke-webrequest\n    # installer分区盘符不一定是D盘\n    # 所以复制 resize.bat 到 install.wim\n    if true; then\n        info \"mount install.wim\"\n        wimmountrw $install_wim \"$image_index\" /wim/\n        if false; then\n            # 使用 autounattend.xml\n            # win7 在此阶段找不到网卡\n            download $confhome/windows-resize.bat /wim/windows-resize.bat\n            for ethx in $(get_eths); do\n                create_win_set_netconf_script /wim/windows-set-netconf-$ethx.bat\n            done\n        else\n            modify_windows /wim\n        fi\n\n        info \"Unmount install.wim\"\n        wimunmount --commit /wim/\n    fi\n\n    # 添加引导\n    if is_efi; then\n        # 现在 add_default_efi_to_nvram() 添加 bootx64.efi 到最前面\n        # 因此这里重复了\n        if false; then\n            apk add efibootmgr\n            efibootmgr -c -L \"Windows Installer\" -d /dev/$xda -p1 -l \"\\\\EFI\\\\boot\\\\$boot_efi\"\n        fi\n    else\n        # 或者用 ms-sys\n        apk add grub-bios\n        # efi 下，强制安装 mbr 引导，需要添加 --target i386-pc\n        grub-install --target i386-pc --boot-directory=\"$(get_path_in_correct_case /os/boot)\" /dev/$xda\n        cat <<EOF >\"$(get_path_in_correct_case /os/boot/grub/grub.cfg)\"\n            set timeout=5\n            menuentry \"reinstall\" {\n                search --no-floppy --label --set=root os\n                ntldr /$(cd /os && get_path_in_correct_case bootmgr)\n            }\nEOF\n    fi\n}\n\n# 添加 netboot.efi 备用\ndownload_netboot_xyz_efi() {\n    dir=$1\n    info \"download netboot.xyz.efi\"\n\n    file=$dir/netboot.xyz.efi\n    if [ \"$(uname -m)\" = aarch64 ]; then\n        download https://boot.netboot.xyz/ipxe/netboot.xyz-arm64.efi $file\n    else\n        download https://boot.netboot.xyz/ipxe/netboot.xyz.efi $file\n    fi\n}\n\nrefind_main_disk() {\n    if true; then\n        apk add sfdisk\n        main_disk=$(sfdisk --disk-id /dev/$xda | sed 's/0x//')\n    else\n        apk add lsblk\n        # main_disk=$(blkid --match-tag PTUUID -o value /dev/$xda)\n        main_disk=$(lsblk --nodeps -rno PTUUID /dev/$xda)\n    fi\n}\n\nsync_time() {\n    if false; then\n        # arm要手动从硬件同步时间，避免访问https出错\n        # do 机器第二次运行会报错\n        hwclock -s || true\n    fi\n\n    # ntp 时间差太多会无法同步？\n    # http 时间可能不准确，毕竟不是专门的时间服务器\n    #      也有可能没有 date header?\n    method=http\n\n    case \"$method\" in\n    ntp)\n        if is_in_china; then\n            ntp_server=ntp.aliyun.com\n        else\n            ntp_server=pool.ntp.org\n        fi\n        # -d[d]   Verbose\n        # -n      Run in foreground\n        # -q      Quit after clock is set\n        # -p      PEER\n        ntpd -d -n -q -p \"$ntp_server\"\n        ;;\n    http)\n        url=\"$(grep -m1 ^http /etc/apk/repositories)/$(uname -m)/APKINDEX.tar.gz\"\n        # 可能有多行，取第一行\n        date_header=$(wget -S --no-check-certificate --spider \"$url\" 2>&1 | grep -m1 '^  Date:')\n        # gnu date 不支持 -D\n        busybox date -u -D \"  Date: %a, %d %b %Y %H:%M:%S GMT\" -s \"$date_header\"\n        ;;\n    esac\n\n    # 重启时 alpine 会自动写入到硬件时钟，因此这里跳过\n    # hwclock -w\n}\n\nis_ubuntu_lts() {\n    IFS=. read -r major minor < <(echo \"$releasever\")\n    [ $((major % 2)) = 0 ] && [ $minor = 04 ]\n}\n\nget_ubuntu_kernel_flavor() {\n    # 20.04/22.04 kvm 内核 vnc 没显示\n    # 24.04 kvm = virtual\n    # linux-image-virtual = linux-image-6.x-generic\n    # linux-image-generic = linux-image-6.x-generic + amd64-microcode + intel-microcode + linux-firmware + linux-modules-extra-generic\n\n    # TODO: ISO virtual-hwe-24.04 不安装 linux-image-extra-virtual-hwe-24.04 不然会花屏\n\n    # https://github.com/systemd/systemd/blob/main/src/basic/virt.c\n    # https://github.com/canonical/cloud-init/blob/main/tools/ds-identify\n    # http://git.annexia.org/?p=virt-what.git;a=blob;f=virt-what.in;hb=HEAD\n    if [ \"$releasever\" = 16.04 ]; then\n        if is_virt; then\n            echo virtual-hwe-$releasever\n        else\n            echo generic-hwe-$releasever\n        fi\n    else\n        # 这里有坑\n        # $(get_cloud_vendor) 调用了 cache_dmi_and_virt\n        # 但是 $(get_cloud_vendor) 运行在 subshell 里面\n        # subshell 运行结束后里面的变量就消失了\n        # 因此先运行 cache_dmi_and_virt\n        cache_dmi_and_virt\n        vendor=\"$(get_cloud_vendor)\"\n        case \"$vendor\" in\n        aws | gcp | oracle | azure | ibm) echo $vendor ;;\n        *)\n            is_ubuntu_lts && suffix=-hwe-$releasever || suffix=\n            if is_virt; then\n                echo virtual$suffix\n            else\n                echo generic$suffix\n            fi\n            ;;\n        esac\n    fi\n}\n\ninstall_redhat_ubuntu() {\n    info \"Download iso installer\"\n\n    # 安装 grub2\n    if is_efi; then\n        # 注意低版本的grub无法启动f38 arm的内核\n        # https://forums.fedoraforum.org/showthread.php?330104-aarch64-pxeboot-vmlinuz-file-format-changed-broke-PXE-installs\n        apk add grub-efi efibootmgr\n        grub-install --efi-directory=/os/boot/efi --boot-directory=/os/boot\n    else\n        apk add grub-bios\n        grub-install --boot-directory=/os/boot /dev/$xda\n    fi\n\n    # 重新整理 extra，因为grub会处理掉引号，要重新添加引号\n    extra_cmdline=''\n    for var in $(grep -o '\\bextra_[^ ]*' /proc/cmdline | xargs); do\n        if [[ \"$var\" = \"extra_main_disk=*\" ]]; then\n            # 重新记录主硬盘\n            refind_main_disk\n            extra_cmdline=\"$extra_cmdline extra_main_disk=$main_disk\"\n        else\n            extra_cmdline=\"$extra_cmdline $(echo $var | sed -E \"s/(extra_[^=]*)=(.*)/\\1='\\2'/\")\"\n        fi\n    done\n\n    # 安装红帽系时，只有最后一个有安装界面显示\n    # https://anaconda-installer.readthedocs.io/en/latest/boot-options.html#console\n    console_cmdline=$(get_ttys console=)\n    grub_cfg=/os/boot/grub/grub.cfg\n\n    # 新版grub不区分linux/linuxefi\n    # shellcheck disable=SC2154\n    if [ \"$distro\" = \"ubuntu\" ]; then\n        download $iso /os/installer/ubuntu.iso\n        mkdir -p /iso\n        mount -o ro /os/installer/ubuntu.iso /iso\n\n        # 内核风味\n        kernel=$(get_ubuntu_kernel_flavor)\n\n        # 要安装的版本\n        # https://canonical-subiquity.readthedocs-hosted.com/en/latest/reference/autoinstall-reference.html#id\n        # 20.04 不能选择 minimal ，也没有 install-sources.yaml\n        source_id=\n        if [ -f /iso/casper/install-sources.yaml ]; then\n            ids=$(grep id: /iso/casper/install-sources.yaml | awk '{print $2}')\n            if [ \"$(echo \"$ids\" | wc -l)\" = 1 ]; then\n                source_id=$ids\n            else\n                [ \"$minimal\" = 1 ] && v= || v=-v\n                source_id=$(echo \"$ids\" | grep $v '\\-minimal')\n\n                if [ \"$(echo \"$source_id\" | wc -l)\" -gt 1 ]; then\n                    error_and_exit \"find multi source id.\"\n                fi\n            fi\n        fi\n\n        # 正常写法应该是 ds=\"nocloud-net;s=https://xxx/\" 但是甲骨文云的ds更优先，自己的ds根本无访问记录\n        # $seed 是 https://xxx/\n        cat <<EOF >$grub_cfg\n        set timeout=5\n        menuentry \"reinstall\" {\n            # https://bugs.launchpad.net/ubuntu/+source/grub2/+bug/1851311\n            # rmmod tpm\n            insmod all_video\n            search --no-floppy --label --set=root installer\n            loopback loop /ubuntu.iso\n            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\n            initrd (loop)/casper/initrd\n        }\nEOF\n    else\n        download $vmlinuz /os/vmlinuz\n        download $initrd /os/initrd.img\n        download $squashfs /os/installer/install.img\n\n        cat <<EOF >$grub_cfg\n        set timeout=5\n        menuentry \"reinstall\" {\n            insmod all_video\n            search --no-floppy --label --set=root os\n            linux /vmlinuz inst.stage2=hd:LABEL=installer:/install.img inst.ks=$ks $extra_cmdline $console_cmdline\n            initrd /initrd.img\n        }\nEOF\n    fi\n\n    cat \"$grub_cfg\"\n}\n\ntrans() {\n    info \"start trans\"\n\n    mod_motd\n\n    # 先检查 modloop 是否正常\n    # 防止格式化硬盘后，缺少 ext4 模块导致 mount 失败\n    # https://github.com/bin456789/reinstall/issues/136\n    ensure_service_started modloop\n\n    cat /proc/cmdline\n    clear_previous\n    add_community_repo\n\n    # 需要在重新分区之前，找到主硬盘\n    # 重新运行脚本时，可指定 xda\n    # xda=sda ash trans.start\n    if [ -z \"$xda\" ]; then\n        find_xda\n    fi\n\n    if [ \"$distro\" != \"alpine\" ]; then\n        setup_web_if_enough_ram\n        # util-linux 包含 lsblk\n        # util-linux 可自动探测 mount 格式\n        apk add util-linux\n    fi\n\n    # dd qemu 切换成云镜像模式，暂时没用到\n    # shellcheck disable=SC2154\n    if [ \"$distro\" = \"dd\" ] && [ \"$img_type\" = \"qemu\" ]; then\n        # 移到 reinstall.sh ?\n        distro=any\n        cloud_image=1\n    fi\n\n    if is_use_cloud_image; then\n        case \"$img_type\" in\n        qemu)\n            create_part\n            download_qcow\n            case \"$distro\" in\n            centos | almalinux | rocky | oracle | redhat | anolis | opencloudos | openeuler)\n                # 这几个系统云镜像系统盘是8~9g xfs，而我们的目标是能在5g硬盘上运行，因此改成复制系统文件\n                install_qcow_by_copy\n                ;;\n            ubuntu)\n                # 24.04 云镜像有 boot 分区（在系统分区之前），因此不直接 dd 云镜像\n                install_qcow_by_copy\n                ;;\n            *)\n                # debian fedora opensuse arch gentoo any\n                dd_qcow\n                resize_after_install_cloud_image\n                modify_os_on_disk linux\n                ;;\n            esac\n            ;;\n        raw)\n            # 暂时没用到 raw 格式的云镜像\n            dd_raw_with_extract\n            resize_after_install_cloud_image\n            modify_os_on_disk linux\n            ;;\n        esac\n    elif [ \"$distro\" = \"dd\" ]; then\n        case \"$img_type\" in\n        raw)\n            dd_raw_with_extract\n            if false; then\n                # linux 扩容后无法轻易缩小，例如 xfs\n                # windows 扩容在 windows 下完成\n                resize_after_install_cloud_image\n            fi\n            if [ -d /configs/cloud-data ]; then\n                modify_os_on_disk nocloud\n            else\n                modify_os_on_disk windows\n            fi\n            ;;\n        qemu) # dd qemu 不可能到这里，因为上面已处理\n            ;;\n        esac\n    else\n        # 安装模式\n        case \"$distro\" in\n        alpine)\n            install_alpine\n            ;;\n        arch | gentoo | aosc)\n            create_part\n            install_arch_gentoo_aosc\n            ;;\n        nixos)\n            create_part\n            install_nixos\n            ;;\n        fnos)\n            create_part\n            install_fnos\n            ;;\n        *)\n            create_part\n            mount_part_for_iso_installer\n            case \"$distro\" in\n            centos | almalinux | rocky | fedora | ubuntu | redhat) install_redhat_ubuntu ;;\n            windows) install_windows ;;\n            esac\n            ;;\n        esac\n    fi\n\n    # 需要用到 lsblk efibootmgr ，只要 1M 左右容量\n    # 因此 alpine 不单独处理\n    if is_efi; then\n        del_invalid_efi_entry\n        add_default_efi_to_nvram\n    fi\n\n    info 'done'\n    # 让 web 输出全部内容\n    sleep 5\n}\n\n# 脚本入口\n# debian initrd 会寻找 main\n# 并调用本文件的 create_ifupdown_config 方法\n: main\n\n# 复制脚本\n# 用于打印错误或者再次运行\n# 路径相同则不用复制\n# 重点：要在删除脚本之前复制\nif ! [ \"$(readlink -f \"$0\")\" = /trans.sh ]; then\n    cp -f \"$0\" /trans.sh\nfi\ntrap 'trap_err $LINENO $?' ERR\n\n# 删除本脚本，不然会被复制到新系统\nrm -f /etc/local.d/trans.start\nrm -f /etc/runlevels/default/local\n\n# 提取变量\nextract_env_from_cmdline\n\n# 带参数运行部分\n# 重新下载并 exec 运行新脚本\nif [ \"$1\" = \"update\" ]; then\n    info 'update script'\n    # shellcheck disable=SC2154\n    wget -O /trans.sh \"$confhome/trans.sh\"\n    chmod +x /trans.sh\n    exec /trans.sh\nelif [ \"$1\" = \"alpine\" ]; then\n    info 'switch to alpine'\n    distro=alpine\n    # 后面的步骤很多都会用到这个，例如分区布局\n    cloud_image=0\nelif [ -n \"$1\" ]; then\n    error_and_exit \"unknown option $1\"\nfi\n\n# 无参数运行部分\n# 允许 ramdisk 使用所有内存，默认是 50%\nmount / -o remount,size=100%\n\n# 同步时间\n# 1. 可以防止访问 https 出错\n# 2. 可以防止 https://github.com/bin456789/reinstall/issues/223\n#    E: Release file for http://security.ubuntu.com/ubuntu/dists/noble-security/InRelease is not valid yet (invalid for another 5h 37min 18s).\n#    Updates for this repository will not be applied.\n# 3. 不能直接读取 rtc，因为默认情况 windows rtc 是本地时间，linux rtc 是 utc 时间\n# 4. 允许同步失败，因为不是关键步骤\nsync_time || true\n\n# 安装 ssh 并更改端口\napk add openssh\nif is_need_change_ssh_port; then\n    change_ssh_port / $ssh_port\nfi\n\n# 设置密码，添加开机启动 + 开启 ssh 服务\nif is_need_set_ssh_keys; then\n    set_ssh_keys_and_del_password /\n    printf '\\n' | setup-sshd\nelse\n    change_root_password /\n    printf '\\nyes' | setup-sshd\nfi\n\n# 设置 frpc\n# 并防止重复运行\nif ls /configs/frpc.* >/dev/null 2>&1 && ! pidof frpc >/dev/null; then\n    info 'run frpc'\n    add_community_repo\n    apk add frp\n    while true; do\n        frpc -c /configs/frpc.* || true\n        sleep 5\n    done &\nfi\n\n# shellcheck disable=SC2154\nif [ \"$hold\" = 1 ]; then\n    if is_run_from_locald; then\n        info \"hold\"\n        exit\n    fi\nfi\n\n# 正式运行重装\n# shellcheck disable=SC2046,SC2194\ncase 1 in\n1)\n    # ChatGPT 说这种性能最高\n    exec > >(exec tee $(get_ttys /dev/) /reinstall.log) 2>&1\n    trans\n    ;;\n2)\n    exec > >(tee $(get_ttys /dev/) /reinstall.log) 2>&1\n    trans\n    ;;\n3)\n    trans 2>&1 | tee $(get_ttys /dev/) /reinstall.log\n    ;;\nesac\n\nif [ \"$hold\" = 2 ]; then\n    info \"hold 2\"\n    exit\nfi\n\n# swapoff -a\n# umount ?\nsync\nreboot\n"
  },
  {
    "path": "ttys.sh",
    "content": "#!/bin/sh\nprefix=$1\n\n# 不要在 windows 上使用，因为不准确\n# 在原系统上使用，也可能不准确？例如安装了 cloud 内核的甲骨文？\n\n# 注意 debian initrd 没有 xargs\n\n# 最后一个 tty 是主 tty，显示的信息最全\nis_first=true\nif [ \"$(uname -m)\" = \"aarch64\" ]; then\n    ttys=\"ttyS0 ttyAMA0 tty0\"\nelse\n    ttys=\"ttyS0 tty0\"\nfi\n\nfor tty in $ttys; do\n    # hytron 有ttyS0 但无法写入\n    if stty -g -F \"/dev/$tty\" >/dev/null 2>&1; then\n        if $is_first; then\n            is_first=false\n        else\n            printf \" \"\n        fi\n\n        printf \"%s\" \"$prefix$tty\"\n\n        if [ \"$prefix\" = \"console=\" ] &&\n            { [ \"$tty\" = ttyS0 ] || [ \"$tty\" = ttyAMA0 ]; }; then\n            printf \",115200n8\"\n        fi\n    fi\ndone\n"
  },
  {
    "path": "ubuntu-storage-early.sh",
    "content": "#!/bin/bash\nsed -i -E '/^\\.{3}$/d' /autoinstall.yaml\necho 'storage:' >>/autoinstall.yaml\n\n# 禁用 swap\ncat <<EOF >>/autoinstall.yaml\n  swap:\n    size: 0\nEOF\n\n# 是用 size 寻找分区，number 没什么用\n# https://curtin.readthedocs.io/en/latest/topics/storage.html\nsize_os=$(lsblk -bn -o SIZE /dev/disk/by-label/os)\n\n# shellcheck disable=SC2154\nif parted \"/dev/$xda\" print | grep '^Partition Table' | grep gpt; then\n    # efi\n    if [ -e /dev/disk/by-label/efi ]; then\n        size_efi=$(lsblk -bn -o SIZE /dev/disk/by-label/efi)\n        cat <<EOF >>/autoinstall.yaml\n  config:\n    # disk\n    - ptable: gpt\n      path: /dev/$xda\n      preserve: true\n      type: disk\n      id: disk-xda\n    # efi 分区\n    - device: disk-xda\n      size: $size_efi\n      number: 1\n      preserve: true\n      grub_device: true\n      type: partition\n      id: partition-efi\n    - fstype: fat32\n      volume: partition-efi\n      type: format\n      id: format-efi\n    # os 分区\n    - device: disk-xda\n      size: $size_os\n      number: 2\n      preserve: true\n      type: partition\n      id: partition-os\n    - fstype: ext4\n      volume: partition-os\n      type: format\n      id: format-os\n    # mount\n    - path: /\n      device: format-os\n      type: mount\n      id: mount-os\n    - path: /boot/efi\n      device: format-efi\n      type: mount\n      id: mount-efi\nEOF\n    else\n        # bios > 2t\n        size_biosboot=$(parted \"/dev/$xda\" unit b print | grep bios_grub | awk '{print $4}' | sed 's/B$//')\n        cat <<EOF >>/autoinstall.yaml\n  config:\n    # disk\n    - ptable: gpt\n      path: /dev/$xda\n      preserve: true\n      grub_device: true\n      type: disk\n      id: disk-xda\n    # biosboot 分区\n    - device: disk-xda\n      size: $size_biosboot\n      number: 1\n      preserve: true\n      type: partition\n      id: partition-biosboot\n    # os 分区\n    - device: disk-xda\n      size: $size_os\n      number: 2\n      preserve: true\n      type: partition\n      id: partition-os\n    - fstype: ext4\n      volume: partition-os\n      type: format\n      id: format-os\n    # mount\n    - path: /\n      device: format-os\n      type: mount\n      id: mount-os\nEOF\n    fi\nelse\n    # bios\n    cat <<EOF >>/autoinstall.yaml\n  config:\n    # disk\n    - ptable: msdos\n      path: /dev/$xda\n      preserve: true\n      grub_device: true\n      type: disk\n      id: disk-xda\n    # os 分区\n    - device: disk-xda\n      size: $size_os\n      number: 1\n      preserve: true\n      type: partition\n      id: partition-os\n    - fstype: ext4\n      volume: partition-os\n      type: format\n      id: format-os\n    # mount\n    - path: /\n      device: format-os\n      type: mount\n      id: mount-os\nEOF\nfi\necho ... >>/autoinstall.yaml\n"
  },
  {
    "path": "ubuntu.yaml",
    "content": "#cloud-config\n# 顺序 early-commands > 安装系统 > late-commands > 重启进入系统 > cloud-init: runcmd > cloud-init: 其他\nautoinstall:\n  version: 1\n  apt:\n    fallback: offline-install\n  source:\n    id: \"@SOURCE_ID@\"\n  kernel:\n    package: linux-generic\n  timezone: Asia/Shanghai\n  ssh:\n    allow-pw: true\n    authorized-keys: []\n    install-server: true\n  early-commands:\n    - |\n      # 解决 20.04 不能识别硬盘\n      # https://askubuntu.com/questions/1302392/ubuntu-server-20-04-setup-stuck-at-block-probing-did-not-discover-any-disks\n      mount | grep /isodevice && { losetup -d /dev/loop0; umount -l /isodevice; } || true\n\n      # 提取 extra_confhome extra_kernel\n      prefix=extra\n      for var in $(grep -o \"\\b${prefix}_[^ ]*\" /proc/cmdline | xargs); do\n          eval \"$(echo $var | sed -E \"s/${prefix}_([^=]*)=(.*)/\\1='\\2'/\")\"\n      done\n\n      # 生成分区信息\n      xda=$(curl -L \"$confhome/get-xda.sh\" | sh -s)\n      export xda\n      curl -L \"$confhome/ubuntu-storage-early.sh\" | sh -s\n\n      # 要安装的版本\n      # 有的镜像只有一个版本，没有 install-sources.yaml\n      # 因此提取不到 $source_id，此时 $source_id 参数为空\n      if [ -n \"$source_id\" ]; then\n          sed -i \"s/@SOURCE_ID@/$source_id/\" /autoinstall.yaml\n      else\n          sed -i \"/@SOURCE_ID@/d\" /autoinstall.yaml\n      fi\n\n      # 内核风味\n      # https://bugs.launchpad.net/subiquity/+bug/1989353\n      sed -i \"s/generic/$kernel/\" /run/kernel-meta-package\n      sed -i \"/package:/s/generic/$kernel/\" /autoinstall.yaml\n\n      # 跳过最后的更新\n      cp /usr/sbin/chroot /usr/sbin/chroot.bin\n      cat >/usr/sbin/chroot <<EOF\n      #!/bin/sh\n      [ \"\\$2\" = \"unattended-upgrades\" ] || /usr/sbin/chroot.bin \"\\$@\"\n      EOF\n\n      # 禁用 DNS  强制离线安装内核和跳过最后的更新\n      # 但安装器会配置时区和写入最近的mirror到/etc/apt/sources.list 所以要提前解析\n      # dig会显示cname结果，cname会以.结尾，grep -v '\\.$' 表示去除 cname 结果\n      # echo $(dig +short geoip.ubuntu.com | grep -v '\\.$' | head -1) geoip.ubuntu.com >>/etc/hosts\n      # sed -i -E 's/(^nameserver )/#\\1/' /etc/resolv.conf\n  late-commands:\n    - |\n      # root ssh 登录\n      echo \"PermitRootLogin yes\" >/target/etc/ssh/sshd_config.d/01-permitrootlogin.conf\n\n      # 还原 DNS\n      # sed -i -E 's/^#(nameserver )/\\1/' /etc/resolv.conf\n\n      # 提取 extra_confhome\n      prefix=extra\n      for var in $(grep -o \"\\b${prefix}_[^ ]*\" /proc/cmdline | xargs); do\n          eval \"$(echo $var | sed -E \"s/${prefix}_([^=]*)=(.*)/\\1='\\2'/\")\"\n      done\n\n      # 下载合并分区脚本\n      cd /target\n      curl -LO $confhome/resize.sh\n\n      # 升级 cloud-init\n      # curtin in-target --target=/target -- apt update\n      # curtin in-target --target=/target -- apt install --only-upgrade cloud-init\n  user-data:\n    runcmd:\n      - |\n        # 合并分区\n        bash /resize.sh\n    disable_root: false\n    users:\n      - name: root\n        lock_passwd: false\n    chpasswd:\n      expire: false\n      # 20.04 arm 需要\n      list: |\n        root:123@@@\n      users:\n        - name: root\n          password: 123@@@\n          type: text\n"
  },
  {
    "path": "windows-allow-ping.bat",
    "content": "@echo off\r\nmode con cp select=437 >nul\r\nsetlocal EnableDelayedExpansion\r\n\r\nrem https://learn.microsoft.com/troubleshoot/windows-server/networking/netsh-advfirewall-firewall-control-firewall-behavior#command-example-4-configure-icmp-settings\r\nrem 旧版命令 netsh firewall set icmpsetting 8 对应的配置是：文件和打印机共享(回显请求 - ICMPv4-In)\r\n\r\nset ICMPv4EchoTypeNum=8\r\nset ICMPv6EchoTypeNum=128\r\n\r\nfor %%i in (4, 6) do (\r\n    netsh advfirewall firewall add rule ^\r\n        name=\"ICMP Echo Request (ICMPv%%i-In)\" ^\r\n        dir=in ^\r\n        action=allow ^\r\n        program=System ^\r\n        protocol=ICMPv%%i:!ICMPv%%iEchoTypeNum!,any\r\n)\r\n\r\nrem 删除此脚本\r\ndel \"%~f0\"\r\n"
  },
  {
    "path": "windows-change-rdp-port.bat",
    "content": "@echo off\r\nmode con cp select=437 >nul\r\n\r\nrem set RdpPort=3333\r\n\r\nrem https://learn.microsoft.com/windows-server/remote/remote-desktop-services/clients/change-listening-port\r\nrem HKLM\\SYSTEM\\CurrentControlSet\\Services\\SharedAccess\\Parameters\\FirewallPolicy\\FirewallRules\r\n\r\nrem RemoteDesktop-Shadow-In-TCP\r\nrem 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|\r\n\r\nrem RemoteDesktop-UserMode-In-TCP\r\nrem 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|\r\n\r\nrem RemoteDesktop-UserMode-In-UDP\r\nrem 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|\r\n\r\nrem 设置端口\r\nreg add \"HKLM\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp\" /v PortNumber /t REG_DWORD /d %RdpPort% /f\r\n\r\nrem 设置防火墙\r\nrem 各个版本的防火墙自带的 rdp 规则略有不同\r\nrem 全部版本都有: program=%SystemRoot%\\system32\\svchost.exe service=TermService\r\nrem win7 还有:    program=System                            service=\r\nrem 以下为并集\r\nfor %%a in (TCP, UDP) do (\r\n    netsh advfirewall firewall add rule ^\r\n        name=\"Remote Desktop - Custom Port (%%a-In)\" ^\r\n        dir=in ^\r\n        action=allow ^\r\n        service=any ^\r\n        protocol=%%a ^\r\n        localport=%RdpPort%\r\n)\r\n\r\nrem 家庭版没有 rdp 服务\r\nsc query TermService\r\nif %errorlevel% == 1060 goto :del\r\n\r\nrem 重启服务 可以用 sc 或者 net\r\nrem UmRdpService 依赖 TermService\r\nrem sc stop 不能处理依赖关系，因此 sc stop TermService 前需要 sc stop UmRdpService\r\nrem net stop 可以处理依赖关系\r\nrem sc stop 是异步的，net stop 不是异步，但有 timeout 时间\r\nrem TermService 运行后，UmRdpService 会自动运行\r\n\r\nrem 如果刚好系统在启动 rdp 服务，则会失败，因此要用 goto 循环\r\nrem The Remote Desktop Services service could not be stopped.\r\n\r\nrem 有的机器会死循环，开机 logo 不断转圈\r\nrem 通过 netstat netstat -ano 可以看到端口已修改成功，但rdp服务不断重启 (pid一直改变)\r\nrem 因此限定重试次数避免死循环\r\n\r\nset retryCount=5\r\n\r\n:restartRDP\r\nif %retryCount% LEQ 0 goto :del\r\nnet stop TermService /y && net start TermService || (\r\n    set /a retryCount-=1\r\n    timeout 10\r\n    goto :restartRDP\r\n)\r\n\r\n:del\r\ndel \"%~f0\"\r\n"
  },
  {
    "path": "windows-del-gpo.bat",
    "content": "@echo off\r\nmode con cp select=437 >nul\r\nsetlocal enabledelayedexpansion\r\n\r\nset \"files[1]=%windir%\\System32\\GroupPolicy\\gpt.ini\"\r\nset \"files[2]=%windir%\\System32\\GroupPolicy\\Machine\\Scripts\\scripts.ini\"\r\n\r\nfor %%i in (1 2) do (\r\n    set \"ini=!files[%%i]!\"\r\n    if exist \"!ini!.orig\" (\r\n        move /y \"!ini!.orig\" \"!ini!\"\r\n    ) else (\r\n        del \"!ini!\"\r\n    )\r\n)\r\n\r\ndel \"%~f0\"\r\n"
  },
  {
    "path": "windows-driver-utils.sh",
    "content": "#!/bin/ash\n# shellcheck shell=dash\n# shellcheck disable=SC3001,SC3003,SC3010\n# reinstall.sh / trans.sh 共用此文件\n\n# grep 无法处理 UTF-16LE 编码的 inf，有以下几种解决方法\n# 1. 使用 ripgrep (rg) 或者 ugrep，但 cygwin 没有\n# 2. grep -a a.b.c.d\n# 3. iconv -f UTF-16 -t UTF-8\n\ndel_inf_comment() {\n    sed 's/;.*//'\n}\n\nsimply_inf() {\n    convert_file_to_utf8 \"$1\" | del_cr | del_inf_comment | trim | del_empty_lines\n}\n\nconvert_file_to_utf8() {\n    # ash 用 * 比较字符串有问题\n    # [[ $'\\xEF\\xBB' = $'\\xEF\\xBB*' ]] && echo 1\n\n    # UTF-16LE without BOM 要处理吗？ windows 支持这种编码的 inf?\n\n    # UTF-16LE with BOM\n    if [ \"$(head -c2 \"$1\")\" = $'\\xFF\\xFE' ]; then\n        # -f UTF-16LE -t UTF-8 会添加 UTF-8 BOM\n        iconv -f UTF-16 -t UTF-8 \"$1\"\n\n    # UTF-8 with BOM\n    elif [ \"$(head -c3 \"$1\")\" = $'\\xEF\\xBB\\xBF' ]; then\n        # busybox sed 不支持\n        # sed '1s/^\\xEF\\xBB\\xBF//' \"$1\"\n        tail -c +4 \"$1\"\n\n    # 其它\n    else\n        cat \"$1\"\n    fi\n}\n\nsimply_inf_word() {\n    # 1 删除引号 \"\n    # 2 删除两边空格\n    # 3 \\ 和 .\\ 换成 /\n    # 4 连续的 / 替换成单个 /\n    # 5 删除最前面的 /\n    sed -E \\\n        -e 's,\",,g' \\\n        -e 's/^[[:space:]]+//' -e 's/[[:space:]]+$//' \\\n        -e 's,\\.?\\\\,/,g' \\\n        -e 's,/+,/,g' \\\n        -e 's,^/,,'\n}\n\n# reinstall.sh 下无法判断 iso 是 32 位还是 64 位，此时 mix_x86_x86_64 为 true\n# trans.sh 下可以判断 iso 是 32 位还是 64 位，此时 mix_x86_x86_64 为 false\nlist_files_from_inf() {\n    local inf=$1\n    local arch=$2 # x86 amd64 arm64\n    local mix_x86_x86_64=$3\n\n    # 所有字段不区分大小写\n    inf_txts=$(simply_inf \"$inf\" | to_lower)\n\n    is_match_section() {\n        local section=$1\n\n        [ \"$line\" = \"[$section]\" ] || [ \"$line\" = \"[$section.$arch]\" ] ||\n            { $mix_x86_x86_64 && [ \"$arch\" = x86 ] && [ \"$line\" = \"[$section.amd64]\" ]; } ||\n            { $mix_x86_x86_64 && [ \"$arch\" = amd64 ] && [ \"$line\" = \"[$section.x86]\" ]; }\n    }\n\n    is_match_catalogfile() {\n        local left\n        left=$(echo \"$line\" | awk -F= '{print $1}' | simply_inf_word)\n\n        # catalogfile.nt 是指所有 nt ?\n        [ \"$left\" = \"catalogfile\" ] ||\n            [ \"$left\" = \"catalogfile.nt\" ] ||\n            [ \"$left\" = \"catalogfile.nt$arch\" ] ||\n            { $mix_x86_x86_64 && [ \"$arch\" = x86 ] && [ \"$left\" = \"catalogfile.ntamd64\" ]; } ||\n            { $mix_x86_x86_64 && [ \"$arch\" = amd64 ] && [ \"$left\" = \"catalogfile.ntx86\" ]; }\n    }\n\n    is_match_manufacturer_arch() {\n        # x86 可写 NT / NTx86, 其它必须明确架构\n        # https://learn.microsoft.com/en-us/windows-hardware/drivers/install/inf-manufacturer-section\n        case \"$arch\" in\n        x86) $mix_x86_x86_64 && regex='NT|NTx86|NTamd64' || regex='NT|NTx86' ;;\n        amd64) $mix_x86_x86_64 && regex='NT|NTx86|NTamd64' || regex='NTamd64' ;;\n        arm64) regex='NTarm64' ;;\n        esac\n\n        # 注意 cut awk 结果不同\n        # 虽然在这里不会造成影响\n        # echo 1 | cut -d, -f2-\n        # 1\n        # echo 1 | awk -F, '{print $2}'\n        # 空白\n\n        echo \"$line\" | awk -F, '{for(i=2;i<=NF;i++) print $i}' | grep -Eiwq \"$regex\"\n    }\n\n    # 还需要从 [Strings] 读取字符串?\n\n    # 0. 检测 inf 是否适合当前架构\n    # 目前没有对比版本号\n\n    ##############################################\n    # 注意这种情况, NTamd64.6.0 为空，表示不支持 6.0\n\n    # [Manufacturer]\n    # %V_INTEL%   = Intel, NTamd64.6.0, NTamd64.6.1.1\n\n    # [Intel.NTamd64.6.0]\n    # ; Empty section.\n\n    # 如果后期改成不从 Manufacturer 获取支持的架构，而是从[]获取支持的架构，注意这种情况\n    # [Intel.NTamd64.10.0.1..22000]\n    ##############################################\n\n    # 例子1\n    # [Manufacturer]\n    # %Amazon% = AWSNVME, NTamd64, NTARM64\n\n    # 例子2\n    # [Manufacturer]\n    # %MyName% = MyName,NTx86.6.0,NTx86.5.1,\n    # .\n    # [MyName.NTx86.6.0] ; Empty section, so this INF does not support\n    # .                  ; NT 6.0 and later.\n    # .\n    # [MyName.NTx86.5.1] ; Used for NT 5.1 and later\n    # .                  ; (but not NT 6.0 and later due to the NTx86.6.0 entry)\n    # %MyDev% = InstallB,hwid\n    # .\n    # [MyName]           ; Empty section, so this INF does not support\n    # .                  ; Win2000\n    # .\n\n    # 例子3\n    # 系统自带的驱动，没有 [Manufacturer]\n\n    # 例子4\n    # C:\\Windows\\INF\\wfcvsc.inf\n    # %StdMfg%=Standard,NTamd64...0x0000001,NTamd64...0x0000002,NTamd64...0x0000003\n\n    in_section=false\n    arch_matched=false\n    has_manufacturer=false\n    # 未添加 IFS= 时，read 会删除行首行尾的空白字符\n    while read -r line; do\n        if [[ \"$line\" = \"[\"* ]]; then\n            is_match_section manufacturer && has_manufacturer=true && in_section=true || in_section=false\n            continue\n        fi\n\n        if $in_section; then\n            if is_match_manufacturer_arch; then\n                arch_matched=true\n                break\n            fi\n        fi\n    done < <(echo \"$inf_txts\")\n\n    if $has_manufacturer && ! $arch_matched; then\n        return 10\n    fi\n\n    # 1. 输出 .inf 文件名\n    basename \"$inf\"\n\n    # 2. 输出 .cat 相对路径\n    # 例子\n    # [version]\n    # CatalogFile = \"xxxxx.cat\"\n    # CatalogFile.NTAMD64=Balloon.cat\n    in_section=false\n    # 未添加 IFS= 时，read 会删除行首行尾的空白字符\n    while read -r line; do\n        if [[ \"$line\" = \"[\"* ]]; then\n            is_match_section version && in_section=true || in_section=false\n            continue\n        fi\n\n        if $in_section && is_match_catalogfile; then\n            echo \"$line\" | awk -F= '{print $2}' | simply_inf_word\n        fi\n    done < <(echo \"$inf_txts\")\n\n    # 3. 获取 SourceDisksNames\n    # 例子\n    # [SourceDisksNames]\n    # 1 = \"Windows NT CD-ROM\",file.tag,, \"\\common\"\n    SourceDisksNames=\n    in_section=false\n    # 未添加 IFS= 时，read 会删除行首行尾的空白字符\n    while read -r line; do\n        if [[ \"$line\" = \"[\"* ]]; then\n            is_match_section sourcedisksnames && in_section=true || in_section=false\n            continue\n        fi\n        # 注意可能有空格和引号\n\n        if $in_section; then\n            local num dir\n            num=$(echo \"$line\" | awk -F= '{print $1}' | simply_inf_word)\n            dir=$(echo \"$line\" | awk -F, '{print $4}' | simply_inf_word)\n            # 每行一条记录\n            if [ -n \"$SourceDisksNames\" ]; then\n                SourceDisksNames=$SourceDisksNames$'\\n'\n            fi\n            SourceDisksNames=\"$SourceDisksNames$num:$dir\"\n        fi\n    done < <(echo \"$inf_txts\")\n\n    # 4. 打印 SourceDisksFiles 的绝对路径\n    # 例子\n    # [SourceDisksFiles]\n    # aha154x.sys = 1 , \"\\x86\" ,,\n    in_section=false\n    # 未添加 IFS= 时，read 会删除行首行尾的空白字符\n    while read -r line; do\n        if [[ \"$line\" = \"[\"* ]]; then\n            is_match_section sourcedisksfiles && in_section=true || in_section=false\n            continue\n        fi\n\n        if $in_section; then\n            file=$(echo \"$line\" | awk -F= '{print $1}' | simply_inf_word)\n            num=$(echo \"$line\" | awk -F'=|,' '{print $2}' | simply_inf_word)\n            sub_dir=$(echo \"$line\" | awk -F, '{print $2}' | simply_inf_word)\n            # 可能有多个\n            while IFS= read -r parent_dir; do\n                echo \"$parent_dir/$sub_dir/$file\" | simply_inf_word\n            done < <(echo \"$SourceDisksNames\" | awk -F: \"\\$1==\\\"$num\\\" {print \\$2}\")\n        fi\n    done < <(echo \"$inf_txts\")\n}\n\n# windows 安装驱动时，只会安装相同架构的驱动文件到系统，即使 inf 里有列出其它架构的驱动\n# 因此 DISM 导出驱动时，也就没有包含其它架构的驱动文件\n\n# 用于尽可能匹配路径大小写\nget_path_in_correct_case() {\n    # 同时支持参数和管道\n    local path\n    path=$({ if [ -n \"$1\" ]; then echo \"$1\"; else cat; fi; })\n\n    # 用 / 分割路径，提取成列表\n    # 例如: ///a///b/c.inf -> a b c.inf\n    # shellcheck disable=SC2046\n    set -- $(echo \"$path\" | grep -o '[^/]*')\n    (\n        local output=\n        if is_absolute_path \"$path\"; then\n            cd /\n            output=/\n        fi\n\n        stop_find=false\n\n        while [ $# -gt 0 ]; do\n            local part=$1\n            local tmp\n            # shellcheck disable=SC2010\n            if ! $stop_find; then\n                if tmp=$(ls -1 | grep -Fix \"$part\"); then\n                    part=$tmp\n                    # 大于 1 表示当前 part 是目录\n                    if [ $# -gt 1 ]; then\n                        if ! cd \"$part\" 2>/dev/null; then\n                            warn \"Can't cd $path\"\n                            stop_find=true\n                        fi\n                    fi\n                else\n                    stop_find=true\n                fi\n            fi\n\n            if [ $# -gt 1 ]; then\n                output=\"$output$part/\"\n            else\n                # 最后 part\n                output=\"$output$part\"\n            fi\n            shift\n        done\n\n        echo \"$output\"\n    )\n}\n\nis_file_or_link() {\n    # -e / -f 坏软连接，返回 false\n    # -L 坏软连接，返回 true\n    [ -f \"$1\" ] || [ -L \"$1\" ]\n}\n\nfind_file_ignore_case() {\n    # 同时支持参数和管道\n    local path\n    path=$({ if [ -n \"$1\" ]; then echo \"$1\"; else cat; fi; })\n\n    path=$(get_path_in_correct_case \"$path\")\n    if is_file_or_link \"$path\"; then\n        echo \"$path\"\n    else\n        warn \"Can't find $path\" >&2\n        return 1\n    fi\n}\n\nparse_inf_and_cp_driever() {\n    local inf=$1\n    local dst=$2\n    local arch=$3\n    local mix_x86_x86_64=$4\n\n    info false \"Add driver: $inf\"\n\n    # 首先创建目录，否则无法通过 ls 文件数得到编号\n    mkdir -p \"$dst\"\n    # shellcheck disable=SC2012\n    inf_index=$(($(ls -1 \"$dst\" | wc -l) + 1))\n    inf_old_dir=$(dirname \"$inf\")\n    inf_new_dir=$dst/$inf_index\n    if driver_files=$(list_files_from_inf \"$inf\" \"$arch\" \"$mix_x86_x86_64\"); then\n        mkdir -p \"$inf_new_dir\"\n        (\n            cd \"$inf_old_dir\" || error_and_exit \"Can't cd $inf_old_dir\"\n            while read -r file; do\n                if file=$(find_file_ignore_case \"$file\"); then\n                    cp -v --parents \"$file\" \"$inf_new_dir\"\n                fi\n            done < <(echo \"$driver_files\")\n        )\n    else\n        if [ $? -eq 10 ]; then\n            warn \"$inf arch not match.\"\n        else\n            error_and_exit \"Unknown error while parse $inf.\"\n        fi\n    fi\n}\n\nget_sys_dir_for_eth() {\n    (\n        cd \"$(readlink -f \"/sys/class/net/$1\")\" || error_and_exit \"Can't cd to $1\"\n        while ! [ \"$(pwd)\" = / ]; do\n            # DRIVER=virtio-pci\n            # PCI_CLASS=20000            # 2 开头表示网络设备\n            # PCI_ID=1AF4:1041\n            # PCI_SUBSYS_ID=1AF4:1100\n            # PCI_SLOT_NAME=0000:03:00.0\n            if [ -f uevent ] && grep -q 'PCI_CLASS=2' uevent && grep -q 'PCI_ID' uevent; then\n                pwd\n                return\n            fi\n            cd ..\n        done\n        return 1\n    )\n}\n"
  },
  {
    "path": "windows-frpc.bat",
    "content": "@echo off\r\nmode con cp select=437 >nul\r\n\r\nrem Windows Deferder 会误报，因此要添加白名单\r\npowershell -ExecutionPolicy Bypass -Command \"Add-MpPreference -ExclusionPath '%SystemDrive%\\frpc\\frpc.exe'\"\r\n\r\nrem 启用日志\r\nrem wevtutil set-log Microsoft-Windows-TaskScheduler/Operational /enabled:true\r\n\r\nrem 创建计划任务并立即运行\r\nschtasks /Create /TN \"frpc\" /XML \"%SystemDrive%\\frpc\\frpc.xml\"\r\nschtasks /Run /TN \"frpc\"\r\ndel \"%SystemDrive%\\frpc\\frpc.xml\"\r\n\r\nrem win10+ 在用户首次登录后，用 LocalService 用户运行的计划任务才会生效\r\nrem 即使手动重启，计划任务也没有运行\r\n\r\nrem 如果 10 秒内有 frpc 进程，则代表计划任务已经生效，不需要首次登录\r\nrem 如果 10 秒后也没有 frpc 进程，则需要临时改用 SYSTEM 用户运行计划任务\r\nfor /L %%i in (1,1,10) do (\r\n    timeout 1\r\n    tasklist /FI \"IMAGENAME eq frpc.exe\" | find /I \"frpc.exe\" && (\r\n        goto :end\r\n    )\r\n)\r\n\r\nrem 临时改用 SYSTEM 用户运行计划任务\r\nschtasks /Change /TN frpc /RU S-1-5-18\r\nschtasks /Run /TN frpc\r\n\r\nrem 用户登录后改回用 LocalService 运行\r\nreg add \"HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce\" /f ^\r\n    /v FrpcRunAsLocalService ^\r\n    /t REG_SZ ^\r\n    /d \"schtasks /Change /TN frpc /RU S-1-5-19\"\r\n\r\n:end\r\nrem 删除此脚本\r\ndel \"%~f0\"\r\n"
  },
  {
    "path": "windows-resize.bat",
    "content": "@echo off\r\nmode con cp select=437 >nul\r\n\r\nset C=%SystemDrive:~0,1%\r\nfor /f \"tokens=2\" %%a in ('echo list vol ^| diskpart ^| findstr \"\\<installer\\>\"') do (echo select vol %%a & echo delete partition) | diskpart\r\nfor /f \"tokens=2\" %%a in ('echo list vol ^| diskpart ^| findstr \"\\<%C%\\>\"') do (echo select vol %%a & echo extend) | diskpart\r\ndel \"%~f0\"\r\n"
  },
  {
    "path": "windows-set-netconf.bat",
    "content": "rem set mac_addr=11:22:33:aa:bb:cc\r\n\r\nrem set ipv4_addr=192.168.1.2/24\r\nrem set ipv4_gateway=192.168.1.1\r\nrem set ipv4_dns1=192.168.1.1\r\nrem set ipv4_dns2=192.168.1.2\r\n\r\nrem set ipv6_addr=2222::2/64\r\nrem set ipv6_gateway=2222::1\r\nrem set ipv6_dns1=::1\r\nrem set ipv6_dns2=::2\r\n\r\n@echo off\r\nmode con cp select=437 >nul\r\n\r\nrem 禁用 IPv6 地址标识符的随机化，防止 IPv6 和后台面板不一致\r\nnetsh interface ipv6 set global randomizeidentifiers=disabled\r\n\r\nrem 检查是否定义了 MAC 地址\r\nif not defined mac_addr goto :del\r\n\r\nrem vista 没有自带 powershell\r\nrem win11 24h2 安装后有 wmic，但是过一段时间会自动删除，因此有的 dd 镜像没有 wmic\r\nif exist \"%windir%\\system32\\wbem\\wmic.exe\" (\r\n    rem wmic 换行符是 \\r\\r\\n\r\n    rem 虽然这里用了 findstr 全字匹配 ，但是结尾还是有 \\r\r\n    for /f \"tokens=2 delims==\" %%a in (\r\n        'wmic nic where \"MACAddress='%mac_addr%'\" get InterfaceIndex /format:list ^| findstr \"^InterfaceIndex=[0-9][0-9]*$\"'\r\n    ) do set id=%%a\r\n)\r\n\r\nif not defined id (\r\n    for /f %%a in ('powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass ^\r\n        -Command \"(Get-WmiObject Win32_NetworkAdapter | Where-Object { $_.MACAddress -eq '%mac_addr%' }).InterfaceIndex\" ^| findstr \"^[0-9][0-9]*$\"'\r\n    ) do set id=%%a\r\n)\r\n\r\nif not defined id (\r\n    for /f %%a in ('powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass ^\r\n        -Command \"(Get-CimInstance Win32_NetworkAdapter | Where-Object { $_.MACAddress -eq '%mac_addr%' }).InterfaceIndex\" ^| findstr \"^[0-9][0-9]*$\"'\r\n    ) do set id=%%a\r\n)\r\n\r\nif defined id (\r\n    rem 配置静态 IPv4 地址和网关\r\n    if defined ipv4_addr if defined ipv4_gateway (\r\n        rem 如果使用了 setlocal EnableDelayedExpansion\r\n        rem netsh interface ipv4 set address !id! static %ipv4_addr% gateway=%ipv4_gateway% gwmetric=0\r\n        rem !id! 变量最后有 \\r 会导致语句不正确\r\n        rem %id% 变量则没有这个问题\r\n\r\n        rem gwmetric 默认值为 1，自动跃点需设为 0\r\n        netsh interface ipv4 set address %id% static %ipv4_addr% gateway=%ipv4_gateway% gwmetric=0\r\n    )\r\n\r\n    rem 配置静态 IPv4 DNS 服务器\r\n    for %%i in (1, 2) do (\r\n        if defined ipv4_dns%%i (\r\n            netsh interface ipv4 add | findstr \"dnsservers\" >nul\r\n            if ErrorLevel 1 (\r\n                rem vista\r\n                setlocal EnableDelayedExpansion\r\n                netsh interface ipv4 add dnsserver %id% !ipv4_dns%%i! %%i\r\n                endlocal\r\n            ) else (\r\n                rem win7\r\n                setlocal EnableDelayedExpansion\r\n                netsh interface ipv4 add dnsservers %id% !ipv4_dns%%i! %%i no\r\n                endlocal\r\n            )\r\n        )\r\n    )\r\n\r\n    rem 配置 IPv6 地址和网关\r\n    if defined ipv6_addr if defined ipv6_gateway (\r\n        netsh interface ipv6 set address %id% %ipv6_addr%\r\n        netsh interface ipv6 add route prefix=::/0 %id% %ipv6_gateway%\r\n    )\r\n\r\n    rem 配置 IPv6 DNS 服务器\r\n    for %%i in (1, 2) do (\r\n        if defined ipv6_dns%%i (\r\n            netsh interface ipv6 add | findstr \"dnsservers\" >nul\r\n            if ErrorLevel 1 (\r\n                rem vista\r\n                setlocal EnableDelayedExpansion\r\n                netsh interface ipv6 add dnsserver %id% !ipv6_dns%%i! %%i\r\n                endlocal\r\n            ) else (\r\n                rem win7\r\n                setlocal EnableDelayedExpansion\r\n                netsh interface ipv6 add dnsservers %id% !ipv6_dns%%i! %%i no\r\n                endlocal\r\n            )\r\n        )\r\n    )\r\n)\r\n\r\n:del\r\nrem 删除此脚本\r\ndel \"%~f0\"\r\n"
  },
  {
    "path": "windows-setup.bat",
    "content": "@echo off\r\nmode con cp select=437 >nul\r\n\r\nrem 还原 setup.exe\r\nrename X:\\setup.exe.disabled setup.exe\r\n\r\nrem 等待 10 秒才自动安装\r\ncls\r\nfor /l %%i in (10,-1,1) do (\r\n    echo Press Ctrl+C within %%i seconds to cancel the automatic installation.\r\n    call :sleep 1000\r\n    cls\r\n)\r\n\r\nrem win7 find 命令在 65001 代码页下有问题，仅限 win 7\r\nrem findstr 就正常，但安装程序又没有 findstr\r\nrem echo a | find \"a\"\r\n\r\nrem 使用高性能模式\r\nrem https://learn.microsoft.com/windows-hardware/manufacture/desktop/capture-and-apply-windows-using-a-single-wim\r\nrem win8 pe 没有 powercfg\r\npowercfg /s SCHEME_MIN 2>nul\r\n\r\nrem 安装 SCSI 驱动\r\nif exist X:\\drivers\\ (\r\n    for /f \"delims=\" %%F in ('dir /s /b \"X:\\drivers\\*.inf\" 2^>nul') do (\r\n        call :drvload_if_scsi \"%%~F\"\r\n    )\r\n\r\n    rem 官网写了可以安装但仅会加载关键驱动\r\n    rem Gcore 的 virtio-gpu 在安装时没有显示\r\n    rem 即使安装时加载了显卡驱动\r\n    rem 进入系统后才有显示\r\n    rem find /i \"viogpudo\" \"%%~F\" >nul\r\n    rem if not errorlevel 1 (\r\n    rem     drvload \"%%~F\"\r\n    rem )\r\n)\r\n\r\nrem 安装自定义 SCSI 驱动\r\nrem 可以用 forfiles /p X:\\custom_drivers /m *.inf /c \"cmd /c echo @path\"\r\nrem 不可以用 for %%F in (\"X:\\custom_drivers\\*\\*.inf\")\r\nif exist X:\\custom_drivers\\ (\r\n    for /f \"delims=\" %%F in ('dir /s /b \"X:\\custom_drivers\\*.inf\" 2^>nul') do (\r\n        call :drvload_if_scsi \"%%~F\"\r\n    )\r\n)\r\n\r\nrem 等待加载分区\r\ncall :sleep 5000\r\necho rescan | diskpart\r\n\r\nrem 判断 efi 还是 bios\r\nrem 或者用 https://learn.microsoft.com/windows-hardware/manufacture/desktop/boot-to-uefi-mode-or-legacy-bios-mode\r\nrem pe 下没有 mountvol\r\necho list vol | diskpart | find \"efi\" && (\r\n    set BootType=efi\r\n) || (\r\n    set BootType=bios\r\n)\r\n\r\nrem 获取 ProductType\r\nrem for /f \"tokens=3\" %%a in ('reg query \"HKLM\\SYSTEM\\CurrentControlSet\\Control\\ProductOptions\" /v ProductType') do (\r\nrem     set \"ProductType=%%a\"\r\nrem )\r\n\r\nrem 获取 BuildNumber\r\nfor /f \"tokens=3\" %%a in ('reg query \"HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\" /v CurrentBuildNumber') do (\r\n    set \"BuildNumber=%%a\"\r\n)\r\n\r\nrem 获取 installer 卷 id\r\nfor /f \"tokens=2\" %%a in ('echo list vol ^| diskpart ^| find \"installer\"') do (\r\n    set \"VolIndex=%%a\"\r\n)\r\n\r\nrem 将 installer 分区设为 Y 盘\r\n(echo select vol %VolIndex% & echo assign letter=Y) | diskpart\r\n\r\nrem 旧版安装程序会自动在 C 盘设置虚拟内存\r\nrem 新版安装程序(24h2)不会自动设置虚拟内存\r\nrem 在 installer 分区创建虚拟内存，不用白不用\r\ncall :createPageFile\r\n\r\nrem 查看虚拟内存\r\nrem wmic pagefile\r\n\r\nrem 获取主硬盘 id\r\nrem vista pe 没有 wmic，因此用 diskpart\r\n(echo select vol %VolIndex% & echo list disk) | diskpart | find \"* Disk \" > X:\\disk.txt\r\nfor /f \"tokens=3\" %%a in (X:\\disk.txt) do (\r\n    set \"DiskIndex=%%a\"\r\n)\r\ndel X:\\disk.txt\r\n\r\nrem 这个变量会被 trans.sh 修改\r\nset is4kn=0\r\nif \"%is4kn%\"==\"1\" (\r\n    set EFISize=260\r\n) else (\r\n    set EFISize=100\r\n)\r\n\r\nrem 重新分区/格式化\r\n(if \"%BootType%\"==\"efi\" (\r\n    echo select disk %DiskIndex%\r\n\r\n    echo select part 1\r\n    echo delete part override\r\n    echo select part 2\r\n    echo delete part override\r\n    echo select part 3\r\n    echo delete part override\r\n\r\n    echo create part efi size=%EFISize%\r\n    echo format fs=fat32 quick\r\n\r\n    echo create part msr size=16\r\n\r\n    echo create part primary\r\n    echo format fs=ntfs quick\r\n    rem echo assign letter=Z\r\n) else (\r\n    echo select disk %DiskIndex%\r\n\r\n    echo select part 1\r\n    rem echo delete part override\r\n    rem echo create part primary\r\n    echo format fs=ntfs quick\r\n    echo active\r\n    rem echo assign letter=Z\r\n)) > X:\\diskpart.txt\r\n\r\nrem 使用 diskpart /s ，出错不会执行剩下的 diskpart 命令\r\ndiskpart /s X:\\diskpart.txt\r\ndel X:\\diskpart.txt\r\n\r\nrem 盘符\r\nrem X boot.wim (ram)\r\nrem Y installer\r\nrem Z os\r\n\r\nrem 旧版安装程序会自动在C盘设置虚拟内存，新版安装程序(24h2)不会\r\nrem 如果不创建虚拟内存，1g 内存的机器安装时会报错/杀进程\r\nif %BuildNumber% GEQ 26040 (\r\n    rem 已经在 installer 分区创建了虚拟内存，约等于 boot.wim 的大小，因此这步不需要\r\n    rem vista/2008 没有删除 boot.wim，200M预留空间-(文件系统占用+驱动占用)后，实测能创建1个64M虚拟内存文件\r\n    rem call :createPageFileOnZ\r\n)\r\n\r\nrem 设置应答文件的主硬盘 id\r\nset \"file=X:\\windows.xml\"\r\nset \"tempFile=X:\\tmp.xml\"\r\n\r\nset \"search=%%disk_id%%\"\r\nset \"replace=%DiskIndex%\"\r\n\r\n(for /f \"delims=\" %%i in (%file%) do (\r\n    set \"line=%%i\"\r\n\r\n    setlocal EnableDelayedExpansion\r\n    echo !line:%search%=%replace%!\r\n    endlocal\r\n\r\n)) > %tempFile%\r\nmove /y %tempFile% %file%\r\n\r\n\r\nrem https://github.com/pbatard/rufus/issues/1990\r\nfor %%a in (RAM TPM SecureBoot) do (\r\n    reg add HKLM\\SYSTEM\\Setup\\LabConfig /t REG_DWORD /v Bypass%%aCheck /d 1 /f\r\n)\r\n\r\nrem 设置\r\nset ForceOldSetup=0\r\nset EnableUnattended=1\r\nset EnableEMS=0\r\n\r\nrem 运行 ramdisk X:\\setup.exe 的话\r\nrem vista 会找不到安装源\r\nrem server 23h2 会无法运行\r\nrem 使用 /installfrom 可以解决?\r\nif \"%ForceOldSetup%\"==\"1\" (\r\n    set setup=Y:\\sources\\setup.exe\r\n) else (\r\n    set setup=Y:\\setup.exe\r\n)\r\n\r\nif \"%EnableUnattended%\"==\"1\" (\r\n    set Unattended=/unattend:X:\\windows.xml\r\n)\r\n\r\nrem 新版安装程序默认开了 Compact OS\r\n\r\nrem 新版安装程序不会创建 BIOS MBR 引导\r\nrem 因此要回退到旧版，或者手动修复 MBR\r\nrem server 2025 + bios 也是\r\nrem 但是 server 2025 官网写支持 bios\r\nrem TODO: 使用 ms-sys 可以不修复？\r\nif %BuildNumber% GEQ 26040 if \"%BootType%\"==\"bios\" (\r\n    rem set ForceOldSetup=1\r\n    bootrec /fixmbr\r\n)\r\n\r\nrem 旧版安装程序不会创建 winre 分区\r\nrem 新版安装程序会创建 winre 分区\r\nrem winre 分区创建在 installer 分区前面\r\nrem 禁止 winre 分区后，winre 储存在 C 盘，依然有效\r\nif %BuildNumber% GEQ 26040 if \"%ForceOldSetup%\"==\"0\" (\r\n    set ResizeRecoveryPartition=/ResizeRecoveryPartition Disable\r\n)\r\n\r\nrem 为 windows server 打开 EMS/SAC\r\nrem 普通 windows 没有自带 SAC 组件，暂不处理\r\nrem 现在通过 trans.sh 准确检测系统是否有 SAC 组件，有则修改 EnableEMS 变量打开 EMS\r\nif \"%EnableEMS%\"==\"1\" (\r\n    rem set EMS=/EMSPort:UseBIOSSettings /EMSBaudRate:115200\r\n    set EMS=/EMSPort:COM1 /EMSBaudRate:115200\r\n)\r\n\r\necho on\r\n%setup% %ResizeRecoveryPartition% %EMS% %Unattended%\r\nexit /b\r\n\r\n:sleep\r\nrem 没有加载网卡驱动，无法用 ping 来等待\r\nrem 没有 timeout 命令\r\nrem timeout /t 10 /nobreak\r\necho wscript.sleep(%~1) > X:\\sleep.vbs\r\ncscript //nologo X:\\sleep.vbs\r\ndel X:\\sleep.vbs\r\nexit /b\r\n\r\n:createPageFile\r\nrem 尽量填满空间，pagefile 默认 64M\r\nfor /l %%i in (1, 1, 100) do (\r\n    wpeutil CreatePageFile /path=Y:\\pagefile%%i.sys >nul 2>nul && echo Created pagefile%%i.sys || exit /b\r\n)\r\nexit /b\r\n\r\n:createPageFileOnZ\r\nwpeutil CreatePageFile /path=Z:\\pagefile.sys /size=512\r\nexit /b\r\n\r\n:drvload_if_scsi\r\nrem 不要查找 Class=SCSIAdapter 因为有些驱动等号两边有空格\r\nfind /i \"SCSIAdapter\" \"%~1\" >nul\r\nif not errorlevel 1 (\r\n    drvload \"%~1\"\r\n)\r\nexit /b\r\n"
  },
  {
    "path": "windows.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<unattend xmlns=\"urn:schemas-microsoft-com:unattend\">\r\n\r\n    <settings pass=\"windowsPE\">\r\n        <component name=\"Microsoft-Windows-Setup\" processorArchitecture=\"%arch%\" publicKeyToken=\"31bf3856ad364e35\" language=\"neutral\" versionScope=\"nonSxS\" xmlns:wcm=\"http://schemas.microsoft.com/WMIConfig/2002/State\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\r\n            <!-- 安装过程可节省一点内存？vista 要删除不然报错 -->\r\n            <EnableFirewall>false</EnableFirewall>\r\n            <UserData>\r\n                <AcceptEula>true</AcceptEula>\r\n                <ProductKey>\r\n                    <Key>%key%</Key>\r\n                </ProductKey>\r\n            </UserData>\r\n            <ImageInstall>\r\n                <OSImage>\r\n                    <InstallFrom>\r\n                        <MetaData wcm:action=\"add\">\r\n                            <Key>/IMAGE/NAME</Key>\r\n                            <Value>%image_name%</Value>\r\n                        </MetaData>\r\n                    </InstallFrom>\r\n                    <InstallTo>\r\n                        <DiskID>%disk_id%</DiskID>\r\n                        <PartitionID>%installto_partitionid%</PartitionID>\r\n                    </InstallTo>\r\n                </OSImage>\r\n            </ImageInstall>\r\n            <!-- 用注册表无法绕过 cpu 核数限制 -->\r\n            <!-- https://github.com/pbatard/rufus/issues/1990 -->\r\n            <!-- https://learn.microsoft.com/windows/iot/iot-enterprise/Hardware/System_Requirements -->\r\n            <!-- <RunSynchronous>\r\n                <RunSynchronousCommand wcm:action=\"add\">\r\n                    <Order>1</Order>\r\n                    <Path>cmd /c for %a in (RAM TPM SecureBoot) do reg add HKLM\\SYSTEM\\Setup\\LabConfig /t REG_DWORD /v Bypass%aCheck /d 1 /f</Path>\r\n                </RunSynchronousCommand>\r\n            </RunSynchronous> -->\r\n        </component>\r\n        <component name=\"Microsoft-Windows-International-Core-WinPE\" processorArchitecture=\"%arch%\" publicKeyToken=\"31bf3856ad364e35\" language=\"neutral\" versionScope=\"nonSxS\" xmlns:wcm=\"http://schemas.microsoft.com/WMIConfig/2002/State\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\r\n            <InputLocale>%locale%</InputLocale>\r\n            <SystemLocale>%locale%</SystemLocale>\r\n            <UILanguage>%locale%</UILanguage>\r\n            <UserLocale>%locale%</UserLocale>\r\n        </component>\r\n    </settings>\r\n\r\n    <!-- vultr win8/8.1/2012/2012r2 在 windowsPE/Microsoft-Windows-PnpCustomizationsWinPE 下载加载气球驱动\r\n    会出现 Windows Cannot find Microsoft software license terms 错误 -->\r\n\r\n    <settings pass=\"offlineServicing\">\r\n        <component name=\"Microsoft-Windows-PnpCustomizationsNonWinPE\" processorArchitecture=\"%arch%\" publicKeyToken=\"31bf3856ad364e35\" language=\"neutral\" versionScope=\"nonSxS\" xmlns:wcm=\"http://schemas.microsoft.com/WMIConfig/2002/State\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\r\n            <DriverPaths>\r\n                <!-- 目录为空不会报错 -->\r\n                <!-- 驱动可以放在子目录 -->\r\n                <PathAndCredentials wcm:action=\"add\" wcm:keyValue=\"1\">\r\n                    <Path>X:\\drivers</Path>\r\n                </PathAndCredentials>\r\n                <PathAndCredentials wcm:action=\"add\" wcm:keyValue=\"2\">\r\n                    <Path>X:\\custom_drivers</Path>\r\n                </PathAndCredentials>\r\n            </DriverPaths>\r\n        </component>\r\n    </settings>\r\n\r\n    <settings pass=\"specialize\">\r\n        <component name=\"Microsoft-Windows-Deployment\" processorArchitecture=\"%arch%\" publicKeyToken=\"31bf3856ad364e35\" language=\"neutral\" versionScope=\"nonSxS\" xmlns:wcm=\"http://schemas.microsoft.com/WMIConfig/2002/State\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\r\n            <RunSynchronous>\r\n                <!-- 大厂都要求设置高性能和从不关闭显示器\r\n                https://learn.microsoft.com/en-us/azure/virtual-machines/windows/prepare-for-upload-vhd-image#set-windows-configurations-for-azure\r\n                https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/migrating-latest-types.html\r\n                https://support.huaweicloud.com/usermanual-ims/zh-cn_topic_0047501112.html\r\n                powercfg /aliases 能显示 GUID/名称 的对应关系，vista 也能使用名称 -->\r\n                <RunSynchronousCommand wcm:action=\"add\">\r\n                    <Order>1</Order>\r\n                    <Path>powercfg /setacvalueindex SCHEME_BALANCED SUB_VIDEO VIDEOIDLE 0</Path>\r\n                </RunSynchronousCommand>\r\n                <RunSynchronousCommand wcm:action=\"add\">\r\n                    <Order>2</Order>\r\n                    <Path>powercfg /setacvalueindex SCHEME_MIN SUB_VIDEO VIDEOIDLE 0</Path>\r\n                </RunSynchronousCommand>\r\n                <RunSynchronousCommand wcm:action=\"add\">\r\n                    <Order>3</Order>\r\n                    <Path>powercfg /setacvalueindex SCHEME_MAX SUB_VIDEO VIDEOIDLE 0</Path>\r\n                </RunSynchronousCommand>\r\n                <!-- 设为高性能 -->\r\n                <RunSynchronousCommand wcm:action=\"add\">\r\n                    <Order>4</Order>\r\n                    <Path>powercfg /setactive SCHEME_MIN</Path>\r\n                </RunSynchronousCommand>\r\n                <!-- 启用 administrator 账户 -->\r\n                <RunSynchronousCommand wcm:action=\"add\">\r\n                    <Order>5</Order>\r\n                    <!-- vista 没有自带 powershell -->\r\n                    <!-- <Path>powershell \"$User = Get-WmiObject Win32_UserAccount | where SID -like *-500; $User.Disabled = $False; $User.Put()\"</Path> -->\r\n                    <!-- win7 此时无法用 wmic useraccount -->\r\n                    <!-- <Path>wmic useraccount where \"sid like '%-500'\" set Disabled=false</Path> -->\r\n                    <!-- https://learn.microsoft.com/archive/technet-wiki/13813.localized-names-for-administrator-account-in-windows -->\r\n                    <Path>cmd /c \"for %a in (Administrator Administrador Administrateur Administratör Администратор Järjestelmänvalvoja Rendszergazda) do (net user %a /active:yes &amp;&amp; exit)\"</Path>\r\n                </RunSynchronousCommand>\r\n                <!-- 禁用保留空间 -->\r\n                <RunSynchronousCommand wcm:action=\"add\">\r\n                    <Order>6</Order>\r\n                    <Path>reg add HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\ReserveManager /v ShippedWithReserves /t REG_DWORD /d 0 /f</Path>\r\n                    <!-- 此时以下命令无效 -->\r\n                    <!-- <Path>DISM /Online /Set-ReservedStorageState /State:Disabled</Path> -->\r\n                    <!-- <Path>powershell \"Set-WindowsReservedStorageState -State Disabled; exit\"</Path> -->\r\n                </RunSynchronousCommand>\r\n                <!-- win7 在此阶段找不到网卡 -->\r\n                <!-- <RunSynchronousCommand wcm:action=\"add\">\r\n                    <Order>5</Order>\r\n                    <Path>cmd /c \"if exist %SystemDrive%\\windows-resize.bat (call %SystemDrive%\\windows-resize.bat)\"</Path>\r\n                </RunSynchronousCommand>\r\n                <RunSynchronousCommand wcm:action=\"add\">\r\n                    <Order>6</Order>\r\n                    <Path>cmd /c \"if exist %SystemDrive%\\windows-set-netconf.bat (call %SystemDrive%\\windows-set-netconf.bat)\"</Path>\r\n                </RunSynchronousCommand> -->\r\n            </RunSynchronous>\r\n        </component>\r\n        <component name=\"Microsoft-Windows-TerminalServices-LocalSessionManager\" processorArchitecture=\"%arch%\" publicKeyToken=\"31bf3856ad364e35\" language=\"neutral\" versionScope=\"nonSxS\" xmlns:wcm=\"http://schemas.microsoft.com/WMIConfig/2002/State\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\r\n            <fDenyTSConnections>false</fDenyTSConnections>\r\n        </component>\r\n        <component name=\"Networking-MPSSVC-Svc\" processorArchitecture=\"%arch%\" publicKeyToken=\"31bf3856ad364e35\" language=\"neutral\" versionScope=\"nonSxS\" xmlns:wcm=\"http://schemas.microsoft.com/WMIConfig/2002/State\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\r\n            <FirewallGroups>\r\n                <FirewallGroup wcm:action=\"add\" wcm:keyValue=\"RemoteDesktop\">\r\n                    <Profile>all</Profile>\r\n                    <!-- HKLM\\System\\CurrentControlSet\\Services\\SharedAccess\\Parameters\\FirewallPolicy\\FirewallRules -->\r\n                    <!-- Get-NetFirewallRule | Where-Object Name -like 'RemoteDesktop*' | fl Name,DisplayName,Description,DisplayGroup,Group -->\r\n                    <!-- https://learn.microsoft.com/windows-hardware/customize/desktop/unattend/networking-mpssvc-svc-firewallgroups-firewallgroup-group -->\r\n                    <Group>@FirewallAPI.dll,-28752</Group>\r\n                    <Active>%use_default_rdp_port%</Active>\r\n                </FirewallGroup>\r\n            </FirewallGroups>\r\n        </component>\r\n        <component name=\"Microsoft-Windows-powercpl\" processorArchitecture=\"%arch%\" publicKeyToken=\"31bf3856ad364e35\" language=\"neutral\" versionScope=\"nonSxS\" xmlns:wcm=\"http://schemas.microsoft.com/WMIConfig/2002/State\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\r\n            <PreferredPlan>8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c</PreferredPlan>\r\n        </component>\r\n        <component name=\"Microsoft-Windows-ErrorReportingCore\" processorArchitecture=\"%arch%\" publicKeyToken=\"31bf3856ad364e35\" language=\"neutral\" versionScope=\"nonSxS\" xmlns:wcm=\"http://schemas.microsoft.com/WMIConfig/2002/State\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\r\n            <DisableWER>1</DisableWER>\r\n        </component>\r\n        <component name=\"Microsoft-Windows-SQMApi\" processorArchitecture=\"%arch%\" publicKeyToken=\"31bf3856ad364e35\" language=\"neutral\" versionScope=\"nonSxS\" xmlns:wcm=\"http://schemas.microsoft.com/WMIConfig/2002/State\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\r\n            <CEIPEnabled>0</CEIPEnabled>\r\n        </component>\r\n    </settings>\r\n\r\n    <settings pass=\"oobeSystem\">\r\n        <!-- 好像不起作用 -->\r\n        <!-- <component name=\"Microsoft-Windows-WinRE-RecoveryAgent\" processorArchitecture=\"%arch%\" publicKeyToken=\"31bf3856ad364e35\" language=\"neutral\" versionScope=\"nonSxS\" xmlns:wcm=\"http://schemas.microsoft.com/WMIConfig/2002/State\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\r\n            <UninstallWindowsRE>true</UninstallWindowsRE>\r\n        </component> -->\r\n        <component name=\"Microsoft-Windows-Shell-Setup\" processorArchitecture=\"%arch%\" publicKeyToken=\"31bf3856ad364e35\" language=\"neutral\" versionScope=\"nonSxS\" xmlns:wcm=\"http://schemas.microsoft.com/WMIConfig/2002/State\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\r\n            <UserAccounts>\r\n                <AdministratorPassword>\r\n                    <Value>%administrator_password%</Value>\r\n                    <PlainText>false</PlainText>\r\n                </AdministratorPassword>\r\n            </UserAccounts>\r\n            <OOBE>\r\n                <HideEULAPage>true</HideEULAPage>\r\n                <ProtectYourPC>3</ProtectYourPC>\r\n                <SkipMachineOOBE>true</SkipMachineOOBE>\r\n                <SkipUserOOBE>true</SkipUserOOBE>\r\n            </OOBE>\r\n            <!-- 不设置时区，则使用镜像的默认时区 -->\r\n            <!-- <TimeZone>China Standard Time</TimeZone> -->\r\n        </component>\r\n    </settings>\r\n</unattend>\r\n"
  },
  {
    "path": "wmic.ps1",
    "content": "﻿param(\r\n    [string]$Namespace,\r\n    [string]$Class,\r\n    [string]$Filter,\r\n    [string]$Properties\r\n)\r\n\r\n$propertiesToDisplay = if ($Properties) { $Properties.Split(\",\") } else { @(\"*\") }\r\n\r\n$wmiQuery = @{\r\n    Namespace = $Namespace\r\n    Class     = $Class\r\n}\r\n\r\nif ($Filter) {\r\n    $wmiQuery.Filter = $Filter\r\n}\r\n\r\nGet-WmiObject @wmiQuery | ForEach-Object {\r\n    $_.PSObject.Properties | Where-Object {\r\n        -not $_.Name.StartsWith(\"__\") -and\r\n        ($propertiesToDisplay -contains $_.Name -or $propertiesToDisplay -contains \"*\")\r\n    } | ForEach-Object {\r\n        $name = $_.Name\r\n        $value = $_.Value\r\n\r\n        # 改成 wmic 的输出格式\r\n        if ($value -is [Array]) {\r\n            $formattedValue = ($value | ForEach-Object { \"`\"$_`\"\" }) -join \",\"\r\n            Write-Output \"$name={$formattedValue}\"\r\n        }\r\n        else {\r\n            Write-Output \"$name=$value\"\r\n        }\r\n    }\r\n}\r\n"
  }
]