├── .gitignore ├── LICENSE ├── LICENSE.CC-BY-SA-4.0 ├── README.md ├── dist ├── com.solus_project.Installer.desktop ├── install-symbolic.svg ├── org.freedesktop.policykit.pkexec.policy └── os-installer-wrapper ├── landing.png ├── os-installer-gtk ├── os_installer2 ├── __init__.py ├── application.py ├── data │ ├── install-complete.png │ ├── install-complete.svg │ ├── install-solus-192-arc-style.png │ ├── install-solus-192-arc-style.svg │ ├── livepreview-192-arc-style.png │ ├── livepreview-192-arc-style.svg │ ├── picklocation-192-arc-style.png │ ├── picklocation-192-arc-style.svg │ └── styling.css ├── diskman.py ├── diskops.py ├── mainwindow.py ├── pages │ ├── __init__.py │ ├── basepage.py │ ├── complete.py │ ├── disk_location.py │ ├── geoip.py │ ├── keyboard.py │ ├── language.py │ ├── location.py │ ├── partitioning.py │ ├── progress.py │ ├── summary.py │ ├── system.py │ ├── timezone.py │ └── users.py ├── permissions.py ├── postinstall.py ├── strategy.py ├── tz.py └── users.py ├── setup.py └── test.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build/ 3 | *.egg-info/* 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. -------------------------------------------------------------------------------- /LICENSE.CC-BY-SA-4.0: -------------------------------------------------------------------------------- 1 | Attribution-ShareAlike 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More_considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution-ShareAlike 4.0 International Public 58 | License 59 | 60 | By exercising the Licensed Rights (defined below), You accept and agree 61 | to be bound by the terms and conditions of this Creative Commons 62 | Attribution-ShareAlike 4.0 International Public License ("Public 63 | License"). To the extent this Public License may be interpreted as a 64 | contract, You are granted the Licensed Rights in consideration of Your 65 | acceptance of these terms and conditions, and the Licensor grants You 66 | such rights in consideration of benefits the Licensor receives from 67 | making the Licensed Material available under these terms and 68 | conditions. 69 | 70 | 71 | Section 1 -- Definitions. 72 | 73 | a. Adapted Material means material subject to Copyright and Similar 74 | Rights that is derived from or based upon the Licensed Material 75 | and in which the Licensed Material is translated, altered, 76 | arranged, transformed, or otherwise modified in a manner requiring 77 | permission under the Copyright and Similar Rights held by the 78 | Licensor. For purposes of this Public License, where the Licensed 79 | Material is a musical work, performance, or sound recording, 80 | Adapted Material is always produced where the Licensed Material is 81 | synched in timed relation with a moving image. 82 | 83 | b. Adapter's License means the license You apply to Your Copyright 84 | and Similar Rights in Your contributions to Adapted Material in 85 | accordance with the terms and conditions of this Public License. 86 | 87 | c. BY-SA Compatible License means a license listed at 88 | creativecommons.org/compatiblelicenses, approved by Creative 89 | Commons as essentially the equivalent of this Public License. 90 | 91 | d. Copyright and Similar Rights means copyright and/or similar rights 92 | closely related to copyright including, without limitation, 93 | performance, broadcast, sound recording, and Sui Generis Database 94 | Rights, without regard to how the rights are labeled or 95 | categorized. For purposes of this Public License, the rights 96 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 97 | Rights. 98 | 99 | e. Effective Technological Measures means those measures that, in the 100 | absence of proper authority, may not be circumvented under laws 101 | fulfilling obligations under Article 11 of the WIPO Copyright 102 | Treaty adopted on December 20, 1996, and/or similar international 103 | agreements. 104 | 105 | f. Exceptions and Limitations means fair use, fair dealing, and/or 106 | any other exception or limitation to Copyright and Similar Rights 107 | that applies to Your use of the Licensed Material. 108 | 109 | g. License Elements means the license attributes listed in the name 110 | of a Creative Commons Public License. The License Elements of this 111 | Public License are Attribution and ShareAlike. 112 | 113 | h. Licensed Material means the artistic or literary work, database, 114 | or other material to which the Licensor applied this Public 115 | License. 116 | 117 | i. Licensed Rights means the rights granted to You subject to the 118 | terms and conditions of this Public License, which are limited to 119 | all Copyright and Similar Rights that apply to Your use of the 120 | Licensed Material and that the Licensor has authority to license. 121 | 122 | j. Licensor means the individual(s) or entity(ies) granting rights 123 | under this Public License. 124 | 125 | k. Share means to provide material to the public by any means or 126 | process that requires permission under the Licensed Rights, such 127 | as reproduction, public display, public performance, distribution, 128 | dissemination, communication, or importation, and to make material 129 | available to the public including in ways that members of the 130 | public may access the material from a place and at a time 131 | individually chosen by them. 132 | 133 | l. Sui Generis Database Rights means rights other than copyright 134 | resulting from Directive 96/9/EC of the European Parliament and of 135 | the Council of 11 March 1996 on the legal protection of databases, 136 | as amended and/or succeeded, as well as other essentially 137 | equivalent rights anywhere in the world. 138 | 139 | m. You means the individual or entity exercising the Licensed Rights 140 | under this Public License. Your has a corresponding meaning. 141 | 142 | 143 | Section 2 -- Scope. 144 | 145 | a. License grant. 146 | 147 | 1. Subject to the terms and conditions of this Public License, 148 | the Licensor hereby grants You a worldwide, royalty-free, 149 | non-sublicensable, non-exclusive, irrevocable license to 150 | exercise the Licensed Rights in the Licensed Material to: 151 | 152 | a. reproduce and Share the Licensed Material, in whole or 153 | in part; and 154 | 155 | b. produce, reproduce, and Share Adapted Material. 156 | 157 | 2. Exceptions and Limitations. For the avoidance of doubt, where 158 | Exceptions and Limitations apply to Your use, this Public 159 | License does not apply, and You do not need to comply with 160 | its terms and conditions. 161 | 162 | 3. Term. The term of this Public License is specified in Section 163 | 6(a). 164 | 165 | 4. Media and formats; technical modifications allowed. The 166 | Licensor authorizes You to exercise the Licensed Rights in 167 | all media and formats whether now known or hereafter created, 168 | and to make technical modifications necessary to do so. The 169 | Licensor waives and/or agrees not to assert any right or 170 | authority to forbid You from making technical modifications 171 | necessary to exercise the Licensed Rights, including 172 | technical modifications necessary to circumvent Effective 173 | Technological Measures. For purposes of this Public License, 174 | simply making modifications authorized by this Section 2(a) 175 | (4) never produces Adapted Material. 176 | 177 | 5. Downstream recipients. 178 | 179 | a. Offer from the Licensor -- Licensed Material. Every 180 | recipient of the Licensed Material automatically 181 | receives an offer from the Licensor to exercise the 182 | Licensed Rights under the terms and conditions of this 183 | Public License. 184 | 185 | b. Additional offer from the Licensor -- Adapted Material. 186 | Every recipient of Adapted Material from You 187 | automatically receives an offer from the Licensor to 188 | exercise the Licensed Rights in the Adapted Material 189 | under the conditions of the Adapter's License You apply. 190 | 191 | c. No downstream restrictions. You may not offer or impose 192 | any additional or different terms or conditions on, or 193 | apply any Effective Technological Measures to, the 194 | Licensed Material if doing so restricts exercise of the 195 | Licensed Rights by any recipient of the Licensed 196 | Material. 197 | 198 | 6. No endorsement. Nothing in this Public License constitutes or 199 | may be construed as permission to assert or imply that You 200 | are, or that Your use of the Licensed Material is, connected 201 | with, or sponsored, endorsed, or granted official status by, 202 | the Licensor or others designated to receive attribution as 203 | provided in Section 3(a)(1)(A)(i). 204 | 205 | b. Other rights. 206 | 207 | 1. Moral rights, such as the right of integrity, are not 208 | licensed under this Public License, nor are publicity, 209 | privacy, and/or other similar personality rights; however, to 210 | the extent possible, the Licensor waives and/or agrees not to 211 | assert any such rights held by the Licensor to the limited 212 | extent necessary to allow You to exercise the Licensed 213 | Rights, but not otherwise. 214 | 215 | 2. Patent and trademark rights are not licensed under this 216 | Public License. 217 | 218 | 3. To the extent possible, the Licensor waives any right to 219 | collect royalties from You for the exercise of the Licensed 220 | Rights, whether directly or through a collecting society 221 | under any voluntary or waivable statutory or compulsory 222 | licensing scheme. In all other cases the Licensor expressly 223 | reserves any right to collect such royalties. 224 | 225 | 226 | Section 3 -- License Conditions. 227 | 228 | Your exercise of the Licensed Rights is expressly made subject to the 229 | following conditions. 230 | 231 | a. Attribution. 232 | 233 | 1. If You Share the Licensed Material (including in modified 234 | form), You must: 235 | 236 | a. retain the following if it is supplied by the Licensor 237 | with the Licensed Material: 238 | 239 | i. identification of the creator(s) of the Licensed 240 | Material and any others designated to receive 241 | attribution, in any reasonable manner requested by 242 | the Licensor (including by pseudonym if 243 | designated); 244 | 245 | ii. a copyright notice; 246 | 247 | iii. a notice that refers to this Public License; 248 | 249 | iv. a notice that refers to the disclaimer of 250 | warranties; 251 | 252 | v. a URI or hyperlink to the Licensed Material to the 253 | extent reasonably practicable; 254 | 255 | b. indicate if You modified the Licensed Material and 256 | retain an indication of any previous modifications; and 257 | 258 | c. indicate the Licensed Material is licensed under this 259 | Public License, and include the text of, or the URI or 260 | hyperlink to, this Public License. 261 | 262 | 2. You may satisfy the conditions in Section 3(a)(1) in any 263 | reasonable manner based on the medium, means, and context in 264 | which You Share the Licensed Material. For example, it may be 265 | reasonable to satisfy the conditions by providing a URI or 266 | hyperlink to a resource that includes the required 267 | information. 268 | 269 | 3. If requested by the Licensor, You must remove any of the 270 | information required by Section 3(a)(1)(A) to the extent 271 | reasonably practicable. 272 | 273 | b. ShareAlike. 274 | 275 | In addition to the conditions in Section 3(a), if You Share 276 | Adapted Material You produce, the following conditions also apply. 277 | 278 | 1. The Adapter's License You apply must be a Creative Commons 279 | license with the same License Elements, this version or 280 | later, or a BY-SA Compatible License. 281 | 282 | 2. You must include the text of, or the URI or hyperlink to, the 283 | Adapter's License You apply. You may satisfy this condition 284 | in any reasonable manner based on the medium, means, and 285 | context in which You Share Adapted Material. 286 | 287 | 3. You may not offer or impose any additional or different terms 288 | or conditions on, or apply any Effective Technological 289 | Measures to, Adapted Material that restrict exercise of the 290 | rights granted under the Adapter's License You apply. 291 | 292 | 293 | Section 4 -- Sui Generis Database Rights. 294 | 295 | Where the Licensed Rights include Sui Generis Database Rights that 296 | apply to Your use of the Licensed Material: 297 | 298 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 299 | to extract, reuse, reproduce, and Share all or a substantial 300 | portion of the contents of the database; 301 | 302 | b. if You include all or a substantial portion of the database 303 | contents in a database in which You have Sui Generis Database 304 | Rights, then the database in which You have Sui Generis Database 305 | Rights (but not its individual contents) is Adapted Material, 306 | 307 | including for purposes of Section 3(b); and 308 | c. You must comply with the conditions in Section 3(a) if You Share 309 | all or a substantial portion of the contents of the database. 310 | 311 | For the avoidance of doubt, this Section 4 supplements and does not 312 | replace Your obligations under this Public License where the Licensed 313 | Rights include other Copyright and Similar Rights. 314 | 315 | 316 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 317 | 318 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 319 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 320 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 321 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 322 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 323 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 324 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 325 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 326 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 327 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 328 | 329 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 330 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 331 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 332 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 333 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 334 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 335 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 336 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 337 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 338 | 339 | c. The disclaimer of warranties and limitation of liability provided 340 | above shall be interpreted in a manner that, to the extent 341 | possible, most closely approximates an absolute disclaimer and 342 | waiver of all liability. 343 | 344 | 345 | Section 6 -- Term and Termination. 346 | 347 | a. This Public License applies for the term of the Copyright and 348 | Similar Rights licensed here. However, if You fail to comply with 349 | this Public License, then Your rights under this Public License 350 | terminate automatically. 351 | 352 | b. Where Your right to use the Licensed Material has terminated under 353 | Section 6(a), it reinstates: 354 | 355 | 1. automatically as of the date the violation is cured, provided 356 | it is cured within 30 days of Your discovery of the 357 | violation; or 358 | 359 | 2. upon express reinstatement by the Licensor. 360 | 361 | For the avoidance of doubt, this Section 6(b) does not affect any 362 | right the Licensor may have to seek remedies for Your violations 363 | of this Public License. 364 | 365 | c. For the avoidance of doubt, the Licensor may also offer the 366 | Licensed Material under separate terms or conditions or stop 367 | distributing the Licensed Material at any time; however, doing so 368 | will not terminate this Public License. 369 | 370 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 371 | License. 372 | 373 | 374 | Section 7 -- Other Terms and Conditions. 375 | 376 | a. The Licensor shall not be bound by any additional or different 377 | terms or conditions communicated by You unless expressly agreed. 378 | 379 | b. Any arrangements, understandings, or agreements regarding the 380 | Licensed Material not stated herein are separate from and 381 | independent of the terms and conditions of this Public License. 382 | 383 | 384 | Section 8 -- Interpretation. 385 | 386 | a. For the avoidance of doubt, this Public License does not, and 387 | shall not be interpreted to, reduce, limit, restrict, or impose 388 | conditions on any use of the Licensed Material that could lawfully 389 | be made without permission under this Public License. 390 | 391 | b. To the extent possible, if any provision of this Public License is 392 | deemed unenforceable, it shall be automatically reformed to the 393 | minimum extent necessary to make it enforceable. If the provision 394 | cannot be reformed, it shall be severed from this Public License 395 | without affecting the enforceability of the remaining terms and 396 | conditions. 397 | 398 | c. No term or condition of this Public License will be waived and no 399 | failure to comply consented to unless expressly agreed to by the 400 | Licensor. 401 | 402 | d. Nothing in this Public License constitutes or may be interpreted 403 | as a limitation upon, or waiver of, any privileges and immunities 404 | that apply to the Licensor or You, including from the legal 405 | processes of any jurisdiction or authority. 406 | 407 | 408 | ======================================================================= 409 | 410 | Creative Commons is not a party to its public 411 | licenses. Notwithstanding, Creative Commons may elect to apply one of 412 | its public licenses to material it publishes and in those instances 413 | will be considered the “Licensor.” The text of the Creative Commons 414 | public licenses is dedicated to the public domain under the CC0 Public 415 | Domain Dedication. Except for the limited purpose of indicating that 416 | material is shared under a Creative Commons public license or as 417 | otherwise permitted by the Creative Commons policies published at 418 | creativecommons.org/policies, Creative Commons does not authorize the 419 | use of the trademark "Creative Commons" or any other trademark or logo 420 | of Creative Commons without its prior written consent including, 421 | without limitation, in connection with any unauthorized modifications 422 | to any of its public licenses or any other arrangements, 423 | understandings, or agreements concerning use of licensed material. For 424 | the avoidance of doubt, this paragraph does not form part of the 425 | public licenses. 426 | 427 | Creative Commons may be contacted at creativecommons.org. 428 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # os-installer 2 | 3 | os-installer is the official [Solus](https://getsol.us) installer. 4 | 5 | ![logo](https://build.getsol.us/logo.png) 6 | 7 | ## License 8 | 9 | - The icons are the result of successive iterations by [Alexandros Felekidis](https://github.com/alpha/) and [Sam Hewitt](https://github.com/snwh/icons/tree/master/solus-installer). They are available under the terms of the [Creative Commons Attribution-Share Alike](https://creativecommons.org/licenses/by-sa/4.0/) License. Please see the [included license file](COPY.CC-BY-SA-4.0) for more details. 10 | - All code is available under the terms of the GPL-2.0 License. Please see [LICENSE](LICENSE) for more details. -------------------------------------------------------------------------------- /dist/com.solus_project.Installer.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Install OS 3 | Icon=system-software-install 4 | Exec=os-installer-wrapper 5 | Terminal=false 6 | Type=Application 7 | Categories=X-GNOME-Settings-Panel;GTK;System;Settings; 8 | Keywords=Install;SystemSettings; 9 | 10 | -------------------------------------------------------------------------------- /dist/org.freedesktop.policykit.pkexec.policy: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | SolusProject 7 | https://solus-project.com/ 8 | 9 | 10 | Install Solus 11 | Authentication is required to install Solus 12 | 13 | no 14 | no 15 | yes 16 | 17 | /usr/bin/os-installer-gtk 18 | true 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /dist/os-installer-wrapper: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | nohup pkexec /usr/bin/os-installer-gtk 3 | 4 | -------------------------------------------------------------------------------- /landing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsolus/os-installer/a06bc5e4491f64de48366a264fbb50a003d148c2/landing.png -------------------------------------------------------------------------------- /os-installer-gtk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This file is part of os-installer 5 | # 6 | # Copyright 2013-2019 Solus 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | 14 | import sys 15 | import os 16 | from os_installer2.application import InstallerApplication 17 | from os_installer2.permissions import PermissionsManager 18 | from os_installer2 import SOURCE_FILESYSTEM, join_resource_path 19 | from gi.repository import Gdk, GObject, Gtk, Gio 20 | 21 | 22 | def init_css(): 23 | """ Set up the CSS before we throw any windows up """ 24 | try: 25 | f = Gio.File.new_for_path(join_resource_path("styling.css")) 26 | css = Gtk.CssProvider() 27 | css.load_from_file(f) 28 | screen = Gdk.Screen.get_default() 29 | prio = Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION 30 | Gtk.StyleContext.add_provider_for_screen(screen, 31 | css, 32 | prio) 33 | except Exception as e: 34 | print("Error loading CSS: {}".format(e)) 35 | 36 | 37 | if __name__ == "__main__": 38 | if os.geteuid() != 0: 39 | sys.stderr.write("You must be root to use OsInstaller\n") 40 | sys.stderr.flush() 41 | sys.exit(1) 42 | 43 | # No source filesystem? No cookies for you! 44 | if not os.path.exists(SOURCE_FILESYSTEM): 45 | msg = "Source file system is missing, cannot continue.\n\n{}".format( 46 | SOURCE_FILESYSTEM) 47 | 48 | d = Gtk.MessageDialog(parent=None, flags=Gtk.DialogFlags.MODAL, 49 | type=Gtk.MessageType.WARNING, 50 | buttons=Gtk.ButtonsType.CLOSE, 51 | message_format=msg) 52 | 53 | d.run() 54 | d.destroy() 55 | sys.exit(1) 56 | 57 | # Immediately drop permissions before we init GTK 58 | p = PermissionsManager() 59 | p.down_permissions() 60 | 61 | GObject.threads_init() 62 | Gdk.threads_init() 63 | 64 | init_css() 65 | 66 | app = InstallerApplication() 67 | r = app.run(sys.argv) 68 | sys.exit(r) 69 | -------------------------------------------------------------------------------- /os_installer2/__init__.py: -------------------------------------------------------------------------------- 1 | #!/bin/true 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This file is part of os-installer 5 | # 6 | # Copyright 2013-2020 Solus 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | 14 | import gi.repository 15 | import os 16 | import locale 17 | gi.require_version('Gtk', '3.0') 18 | gi.require_version('Gio', '2.0') 19 | gi.require_version('GnomeDesktop', '3.0') 20 | gi.require_version('TimezoneMap', '1.0') 21 | 22 | 23 | # The path of the source filesystem 24 | SOURCE_FILESYSTEM = "/run/initramfs/live/LiveOS/squashfs.img" 25 | 26 | # The guy inside that is actually our filesystem to copy 27 | INNER_FILESYSTEM = "LiveOS/rootfs.img" 28 | 29 | # Absolute minimum size 30 | MB = 1000 * 1000 31 | GB = 1000 * MB 32 | MIN_REQUIRED_SIZE = 10 * GB 33 | 34 | 35 | def get_resource_path(): 36 | bsPath = os.path.dirname(__file__) 37 | return os.path.join(bsPath, "data") 38 | 39 | 40 | def join_resource_path(path): 41 | return os.path.join(get_resource_path(), path) 42 | 43 | 44 | def format_size(size): 45 | """ Get the *abyte size (not mebibyte) format """ 46 | labels = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] 47 | 48 | for i, label in enumerate(labels): 49 | if size < 1000 or i == len(labels) - 1: 50 | return size, label 51 | size = float(size / 1000) 52 | 53 | 54 | def format_size_local(size, double_precision=False): 55 | """ Get the locale appropriate representation of the size """ 56 | numeric, code = format_size(size) 57 | fmt = "%.1f" if not double_precision else "%.2f" 58 | SZ = "{} {}".format(locale.format(fmt, numeric, grouping=True), code) 59 | return SZ 60 | -------------------------------------------------------------------------------- /os_installer2/application.py: -------------------------------------------------------------------------------- 1 | #!/bin/true 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This file is part of os-installer 5 | # 6 | # Copyright 2013-2020 Solus 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | 14 | from .mainwindow import MainWindow 15 | from gi.repository import Gio, Gtk 16 | 17 | APP_ID = "com.solus_project.Installer" 18 | 19 | 20 | class InstallerApplication(Gtk.Application): 21 | 22 | app_window = None 23 | 24 | def __init__(self): 25 | Gtk.Application.__init__(self, 26 | application_id=APP_ID, 27 | flags=Gio.ApplicationFlags.FLAGS_NONE) 28 | self.connect("activate", self.on_activate) 29 | 30 | def on_activate(self, app): 31 | if self.app_window is None: 32 | self.app_window = MainWindow(self) 33 | self.app_window.present() 34 | -------------------------------------------------------------------------------- /os_installer2/data/install-complete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsolus/os-installer/a06bc5e4491f64de48366a264fbb50a003d148c2/os_installer2/data/install-complete.png -------------------------------------------------------------------------------- /os_installer2/data/install-complete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 27 | 31 | 35 | 36 | 39 | 43 | 47 | 48 | 50 | 54 | 58 | 59 | 69 | 72 | 76 | 80 | 81 | 91 | 93 | 97 | 101 | 102 | 112 | 115 | 119 | 123 | 124 | 133 | 143 | 153 | 154 | 185 | 192 | 193 | 195 | 196 | 198 | image/svg+xml 199 | 201 | 202 | 203 | 204 | 205 | 210 | 218 | 225 | 233 | 239 | 245 | 254 | 261 | 270 | 275 | 276 | 277 | -------------------------------------------------------------------------------- /os_installer2/data/install-solus-192-arc-style.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsolus/os-installer/a06bc5e4491f64de48366a264fbb50a003d148c2/os_installer2/data/install-solus-192-arc-style.png -------------------------------------------------------------------------------- /os_installer2/data/install-solus-192-arc-style.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 27 | 31 | 35 | 36 | 39 | 43 | 47 | 48 | 50 | 54 | 58 | 59 | 69 | 72 | 76 | 80 | 81 | 91 | 93 | 97 | 101 | 102 | 112 | 115 | 119 | 123 | 124 | 134 | 143 | 153 | 154 | 185 | 192 | 193 | 195 | 196 | 198 | image/svg+xml 199 | 201 | 202 | 203 | 204 | 205 | 210 | 218 | 224 | 230 | 239 | 246 | 252 | 261 | 262 | 263 | -------------------------------------------------------------------------------- /os_installer2/data/livepreview-192-arc-style.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsolus/os-installer/a06bc5e4491f64de48366a264fbb50a003d148c2/os_installer2/data/livepreview-192-arc-style.png -------------------------------------------------------------------------------- /os_installer2/data/livepreview-192-arc-style.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 26 | 30 | 34 | 35 | 45 | 48 | 52 | 56 | 57 | 67 | 69 | 73 | 77 | 78 | 88 | 91 | 95 | 99 | 100 | 110 | 113 | 117 | 121 | 122 | 133 | 135 | 139 | 143 | 144 | 146 | 150 | 154 | 155 | 165 | 176 | 178 | 182 | 186 | 187 | 198 | 200 | 204 | 208 | 209 | 210 | 241 | 248 | 249 | 251 | 252 | 254 | image/svg+xml 255 | 257 | 258 | 259 | 260 | 261 | 266 | 273 | 278 | 283 | 291 | 297 | 303 | 309 | 317 | 318 | 319 | -------------------------------------------------------------------------------- /os_installer2/data/picklocation-192-arc-style.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsolus/os-installer/a06bc5e4491f64de48366a264fbb50a003d148c2/os_installer2/data/picklocation-192-arc-style.png -------------------------------------------------------------------------------- /os_installer2/data/picklocation-192-arc-style.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 25 | 32 | 34 | 38 | 42 | 43 | 53 | 56 | 60 | 64 | 65 | 75 | 85 | 86 | 119 | 126 | 127 | 129 | 130 | 132 | image/svg+xml 133 | 135 | 136 | 137 | 138 | 139 | 145 | 151 | 157 | 162 | 166 | 172 | 177 | 183 | 189 | 195 | 201 | 207 | 213 | 219 | 225 | 231 | 237 | 243 | 249 | 250 | 255 | 256 | -------------------------------------------------------------------------------- /os_installer2/data/styling.css: -------------------------------------------------------------------------------- 1 | list { 2 | background-image: none; 3 | background-color: transparent; 4 | } 5 | 6 | scrolledwindow { 7 | background-image: none; 8 | background-color: transparent; 9 | } 10 | -------------------------------------------------------------------------------- /os_installer2/mainwindow.py: -------------------------------------------------------------------------------- 1 | #!/bin/true 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This file is part of os-installer 5 | # 6 | # Copyright 2013-2020 Solus 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | from gi.repository import Gtk, GLib, Gdk 14 | from .diskman import DiskManager 15 | from .permissions import PermissionsManager 16 | from .pages.language import InstallerLanguagePage 17 | from .pages.location import InstallerLocationPage 18 | from .pages.geoip import InstallerGeoipPage 19 | from .pages.keyboard import InstallerKeyboardPage 20 | from .pages.timezone import InstallerTimezonePage 21 | from .pages.disk_location import InstallerDiskLocationPage 22 | from .pages.partitioning import InstallerPartitioningPage 23 | from .pages.system import InstallerSystemPage 24 | from .pages.users import InstallerUsersPage 25 | from .pages.summary import InstallerSummaryPage 26 | from .pages.progress import InstallerProgressPage 27 | from .pages.complete import InstallationCompletePage 28 | import sys 29 | import threading 30 | import traceback 31 | import os 32 | 33 | class FancyLabel(Gtk.Label): 34 | 35 | page_id = None 36 | 37 | def __init__(self, page): 38 | Gtk.Label.__init__(self) 39 | self.set_label(page.get_sidebar_title()) 40 | self.page_id = page.get_name() 41 | self.set_halign(Gtk.Align.START) 42 | self.set_property("margin", 6) 43 | self.set_property("margin-start", 24) 44 | self.set_property("margin-end", 24) 45 | self.get_style_context().add_class("dim-label") 46 | 47 | class InstallInfo: 48 | """ For tracking purposes between pages """ 49 | 50 | # Chosen locale 51 | locale = None 52 | locale_sz = None 53 | 54 | # Chosen keyboard 55 | keyboard = None 56 | keyboard_sz = None 57 | 58 | # Main Window reference 59 | owner = None 60 | 61 | # Timezone for the system 62 | timezone = None 63 | timezone_c = None 64 | 65 | # The chosen disk strategy 66 | strategy = None 67 | 68 | # Whether to enable geoip lookups 69 | enable_geoip = False 70 | cached_location = None 71 | cached_timezone = None 72 | 73 | # system hostname 74 | hostname = None 75 | 76 | # Windows was detected 77 | windows_present = False 78 | 79 | users = None 80 | 81 | # Disk prober 82 | prober = None 83 | 84 | # Bootloader target 85 | bootloader = None 86 | bootloader_sz = None 87 | bootloader_install = False 88 | 89 | invalidated = False 90 | 91 | def __init__(self): 92 | self.users = list() 93 | self.bootloader_install = True 94 | 95 | 96 | class MainWindow(Gtk.ApplicationWindow): 97 | 98 | stack = None 99 | installer_stack = None 100 | installer_page = None 101 | application = None 102 | prev_button = None 103 | next_button = None 104 | 105 | box_labels = None 106 | 107 | pages = list() 108 | page_index = 0 109 | 110 | info = None 111 | 112 | disk_manager = None 113 | perms = None 114 | 115 | # Skip direction 116 | skip_forward = False 117 | 118 | can_quit = True 119 | is_final_step = False 120 | 121 | image_step = None 122 | label_step = None 123 | plasma = False 124 | 125 | def quit_handler(self, w, udata=None): 126 | """ Ensure quit stuff is sane ... """ 127 | if not self.can_quit: 128 | True 129 | return False 130 | 131 | def __init__(self, app): 132 | Gtk.ApplicationWindow.__init__(self, application=app) 133 | self.application = app 134 | 135 | settings = Gtk.Settings.get_default() 136 | settings.set_property("gtk-application-prefer-dark-theme", False) 137 | 138 | self.image_step = Gtk.Image.new_from_icon_name("system-software-install", Gtk.IconSize.DIALOG) 139 | self.image_step.set_property("margin", 8) 140 | self.label_step = Gtk.Label.new("") 141 | self.label_step.set_property("margin", 8) 142 | 143 | self.set_title("Install Solus") 144 | 145 | self.headerbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) 146 | self.headerbox.get_style_context().add_class("header-box") 147 | self.headerbox.pack_start(self.image_step, False, False, 0) 148 | # self.headerbox.pack_start(self.label_step, False, False, 0) 149 | self.box_labels = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) 150 | self.box_labels.set_valign(Gtk.Align.START) 151 | self.box_labels.set_property("margin-bottom", 40) 152 | self.box_labels.set_property("margin-top", 20) 153 | self.headerbox.pack_start(self.box_labels, True, True, 0) 154 | 155 | vanity_icon = "start-here-symbolic" 156 | vanity_string = "" 157 | 158 | if os.path.exists("/usr/bin/budgie-panel"): 159 | vanity_icon = "budgie-desktop-symbolic" 160 | vanity_string = "Solus Budgie" 161 | elif os.path.exists("/usr/bin/gnome-shell"): 162 | vanity_icon = "desktop-environment-gnome" 163 | vanity_string = "Solus GNOME" 164 | elif os.path.exists("/usr/bin/mate-panel"): 165 | vanity_icon = "mate" 166 | vanity_string = "Solus MATE" 167 | elif os.path.exists("/usr/bin/plasmashell"): 168 | vanity_icon = "plasma" 169 | vanity_string = "Solus Plasma" 170 | self.plasma = True 171 | else: 172 | vanity_icon = "start-here-solus" 173 | vanity_string = "Solus" 174 | 175 | img_vanity = Gtk.Image.new_from_icon_name(vanity_icon, Gtk.IconSize.LARGE_TOOLBAR) 176 | img_vanity.set_property("margin", 8) 177 | img_vanity.set_property("margin-top", 0) 178 | lab_vanity = Gtk.Label.new(vanity_string) 179 | lab_vanity.set_property("margin-start", 4) 180 | lab_vanity.set_property("margin-end", 8) 181 | lab_vanity.set_property("margin-bottom", 8) 182 | self.headerbox.pack_end(lab_vanity, False, False, 0) 183 | self.headerbox.pack_end(img_vanity, False, False, 0) 184 | 185 | self.set_icon_name("system-software-install") 186 | self.connect("delete-event", self.quit_handler) 187 | 188 | self.set_position(Gtk.WindowPosition.CENTER) 189 | self.set_default_size(768, 500) 190 | 191 | # Main "install" page 192 | self.installer_page = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) 193 | self.installer_stack = Gtk.Stack() 194 | self.installer_page.pack_start(self.installer_stack, True, True, 0) 195 | 196 | self.installer_wrap = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) 197 | self.installer_wrap.pack_start(self.headerbox, False, False, 0) 198 | sep = Gtk.Separator.new(Gtk.Orientation.VERTICAL) 199 | self.installer_wrap.pack_start(sep, False, False, 0) 200 | self.installer_wrap.pack_start(self.installer_page, True, True, 0) 201 | 202 | ltr = Gtk.StackTransitionType.SLIDE_LEFT_RIGHT 203 | self.installer_stack.set_transition_type(ltr) 204 | self.add(self.installer_wrap) 205 | 206 | 207 | # nav buttons 208 | bbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 4) 209 | bbox.set_halign(Gtk.Align.END) 210 | self.prev_button = Gtk.Button.new_with_label("Previous") 211 | self.next_button = Gtk.Button.new_with_label("Next") 212 | bbox.pack_start(self.prev_button, False, False, 0) 213 | bbox.pack_start(self.next_button, False, False, 0) 214 | bbox.set_margin_top(10) 215 | bbox.set_margin_bottom(10) 216 | bbox.set_margin_end(10) 217 | self.prev_button.set_property("margin-start", 4) 218 | self.next_button.set_property("margin-start", 4) 219 | 220 | # sep before nav 221 | sep = Gtk.Separator.new(Gtk.Orientation.HORIZONTAL) 222 | sep.set_margin_top(20) 223 | self.installer_page.pack_end(bbox, False, 0, 0) 224 | self.installer_page.pack_end(sep, False, 0, 0) 225 | 226 | self.info = InstallInfo() 227 | self.info.owner = self 228 | 229 | self.get_style_context().add_class("installer-window") 230 | 231 | # Hook up actions 232 | self.prev_button.connect("clicked", lambda x: self.prev_page()) 233 | self.next_button.connect("clicked", lambda x: self.next_page()) 234 | # Load other pages here into installer_stack 235 | try: 236 | self.add_installer_page(InstallerLanguagePage()) 237 | self.add_installer_page(InstallerLocationPage()) 238 | self.add_installer_page(InstallerGeoipPage()) 239 | self.add_installer_page(InstallerKeyboardPage()) 240 | self.add_installer_page(InstallerTimezonePage()) 241 | self.add_installer_page(InstallerDiskLocationPage()) 242 | self.add_installer_page(InstallerPartitioningPage()) 243 | self.add_installer_page(InstallerSystemPage()) 244 | self.add_installer_page(InstallerUsersPage()) 245 | self.add_installer_page(InstallerSummaryPage(self.plasma)) 246 | self.add_installer_page(InstallerProgressPage()) 247 | self.add_installer_page(InstallationCompletePage()) 248 | except Exception as e: 249 | print("Fatal error during startup: {}".format(e)) 250 | traceback.print_exc(file=sys.stderr) 251 | sys.exit(1) 252 | 253 | # Shared helpers 254 | self.perms = PermissionsManager() 255 | self.disk_manager = DiskManager() 256 | 257 | self.update_current_page() 258 | self.show_all() 259 | 260 | GLib.idle_add(self.start_threads) 261 | 262 | def start_threads(self): 263 | self.set_can_next(False) 264 | start_thr = threading.Thread(target=self.perform_inits) 265 | start_thr.daemon = True 266 | start_thr.start() 267 | return False 268 | 269 | def perform_inits(self): 270 | self.info.owner.get_perms_manager().down_permissions() 271 | """ Force expensive children to init outside main thread """ 272 | for page in self.pages: 273 | try: 274 | page.do_expensive_init() 275 | except Exception as e: 276 | print("Fatal exception initialising: {}".format(e)) 277 | 278 | 279 | def add_installer_page(self, page): 280 | """ Work a page into the set """ 281 | self.installer_stack.add_named(page, page.get_name()) 282 | lab = FancyLabel(page) 283 | self.box_labels.pack_start(lab, False, False, 0) 284 | self.pages.append(page) 285 | 286 | def next_page(self): 287 | """ Move to next page """ 288 | if self.is_final_step: 289 | msg = "Installation will make changes to your disks, and could " \ 290 | "result in data loss.\nDo you wish to install?" 291 | d = Gtk.MessageDialog(parent=self, flags=Gtk.DialogFlags.MODAL, 292 | type=Gtk.MessageType.WARNING, 293 | buttons=Gtk.ButtonsType.OK_CANCEL, 294 | message_format=msg) 295 | 296 | r = d.run() 297 | d.destroy() 298 | if r != Gtk.ResponseType.OK: 299 | return 300 | 301 | self.skip_forward = True 302 | index = self.page_index + 1 303 | if index >= len(self.pages): 304 | return 305 | page = self.pages[index] 306 | if page.is_hidden(): 307 | index += 1 308 | self.page_index = index 309 | self.update_current_page() 310 | 311 | def prev_page(self): 312 | self.skip_forward = False 313 | """ Move to previous page """ 314 | index = self.page_index - 1 315 | if index < 0: 316 | return 317 | page = self.pages[index] 318 | if page.is_hidden(): 319 | index -= 1 320 | self.page_index = index 321 | self.update_current_page() 322 | 323 | def update_current_page(self): 324 | page = self.pages[self.page_index] 325 | self.set_final_step(False) 326 | 327 | if self.page_index == len(self.pages) - 1: 328 | self.set_can_next(False) 329 | else: 330 | # TODO: Have pages check next-ness 331 | self.set_can_next(True) 332 | if self.page_index == 0: 333 | self.set_can_previous(False) 334 | else: 335 | self.set_can_previous(True) 336 | page.prepare(self.info) 337 | 338 | for label in self.box_labels.get_children(): 339 | if label.page_id == page.get_name(): 340 | label.get_style_context().remove_class("dim-label") 341 | else: 342 | label.get_style_context().add_class("dim-label") 343 | 344 | iname = page.get_icon_name(plasma=self.plasma) 345 | self.image_step.set_from_icon_name(iname, 346 | Gtk.IconSize.DIALOG) 347 | # self.image_step.set_pixel_size(32) 348 | self.installer_stack.set_visible_child_name(page.get_name()) 349 | 350 | def set_can_previous(self, can_prev): 351 | self.prev_button.set_sensitive(can_prev) 352 | 353 | def set_can_next(self, can_next): 354 | self.next_button.set_sensitive(can_next) 355 | 356 | def set_final_step(self, final): 357 | """ Mark this as the final step, should also 358 | add a prompt on selection """ 359 | if final: 360 | self.next_button.set_label("Install") 361 | else: 362 | self.next_button.set_label("Next") 363 | self.is_final_step = final 364 | 365 | def set_can_quit(self, can_quit): 366 | """ Override quit handling """ 367 | self.can_quit = can_quit 368 | if not self.can_quit: 369 | self.prev_button.hide() 370 | self.next_button.hide() 371 | # self.set_deletable(False) 372 | else: 373 | self.prev_button.show_all() 374 | self.next_button.show_all() 375 | # self.set_deletable(True) 376 | 377 | def get_disk_manager(self): 378 | """ Return our disk manager object """ 379 | return self.disk_manager 380 | 381 | def get_perms_manager(self): 382 | """ Return permission manager """ 383 | return self.perms 384 | 385 | def skip_page(self): 386 | GLib.idle_add(self._skip_page) 387 | 388 | def _skip_page(self): 389 | """ Allow pages to request skipping to next page """ 390 | if self.skip_forward: 391 | self.next_page() 392 | else: 393 | self.prev_page() 394 | return False 395 | -------------------------------------------------------------------------------- /os_installer2/pages/__init__.py: -------------------------------------------------------------------------------- 1 | #!/bin/true 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This file is part of os-installer 5 | # 6 | # Copyright 2013-2020 Solus 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | 14 | # Credit: gnome-inital-setup 15 | default_locales = [ 16 | "en_US.UTF-8", 17 | "de_DE.UTF-8", 18 | "fr_FR.UTF-8", 19 | "es_ES.UTF-8", 20 | "zh_CN.UTF-8", 21 | "ja_JP.UTF-8", 22 | "ru_RU.UTF-8", 23 | "ar_EG.UTF-8" 24 | ] 25 | -------------------------------------------------------------------------------- /os_installer2/pages/basepage.py: -------------------------------------------------------------------------------- 1 | #!/bin/true 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This file is part of os-installer 5 | # 6 | # Copyright 2013-2020 Solus 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | 14 | from gi.repository import Gtk 15 | 16 | 17 | class BasePage(Gtk.Box): 18 | """ Base widget for all page implementations to save on duplication. """ 19 | 20 | def __init__(self): 21 | Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=0) 22 | 23 | mk = u"{}".format(self.get_title()) 24 | lab = Gtk.Label.new(mk) 25 | lab.set_property("margin-top", 10) 26 | lab.set_property("margin-start", 20) 27 | lab.set_property("margin-bottom", 10) 28 | lab.set_use_markup(True) 29 | lab.set_halign(Gtk.Align.START) 30 | self.pack_start(lab, False, False, 0) 31 | 32 | sep = Gtk.Separator.new(Gtk.Orientation.HORIZONTAL) 33 | self.pack_start(sep, False, False, 0) 34 | 35 | def get_title(self): 36 | return None 37 | 38 | def get_sidebar_title(self): 39 | return "Not implemented.." 40 | 41 | def get_name(self): 42 | return None 43 | 44 | def get_icon_name(self, plasma=False): 45 | return "dialog-error" 46 | 47 | def get_primary_answer(self): 48 | return None 49 | 50 | def prepare(self, info): 51 | pass 52 | 53 | def seed(self, setup): 54 | pass 55 | 56 | def is_hidden(self): 57 | return False 58 | 59 | def do_expensive_init(self): 60 | """ Do expensive startup tasks outside of the main thread """ 61 | pass 62 | -------------------------------------------------------------------------------- /os_installer2/pages/complete.py: -------------------------------------------------------------------------------- 1 | #!/bin/true 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This file is part of os-installer 5 | # 6 | # Copyright 2013-2020 Solus 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | 14 | from .basepage import BasePage 15 | from gi.repository import Gtk 16 | from os_installer2 import join_resource_path as jrp 17 | import dbus 18 | 19 | 20 | class InstallationCompletePage(BasePage): 21 | """ Last page seen by users """ 22 | 23 | def __init__(self): 24 | BasePage.__init__(self) 25 | 26 | box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) 27 | self.pack_start(box, True, True, 0) 28 | box.set_border_width(40) 29 | box.set_valign(Gtk.Align.CENTER) 30 | box.set_halign(Gtk.Align.CENTER) 31 | 32 | img = Gtk.Image.new_from_file(jrp("install-complete.png")) 33 | img.set_halign(Gtk.Align.CENTER) 34 | box.pack_start(img, False, False, 0) 35 | 36 | lab = Gtk.Label("You may now exit the installer.") 37 | lab.set_use_markup(True) 38 | lab.set_halign(Gtk.Align.CENTER) 39 | lab.set_property("xalign", 0.5) 40 | box.pack_start(lab, False, False, 0) 41 | lab.set_property("margin-top", 5) 42 | lab.set_property("margin-bottom", 5) 43 | 44 | lab = Gtk.Label("Restart and then remove any installation media to " 45 | "start using your new operating system.") 46 | lab.set_use_markup(True) 47 | lab.set_halign(Gtk.Align.CENTER) 48 | lab.set_property("xalign", 0.5) 49 | box.pack_start(lab, False, False, 0) 50 | 51 | # Reboot button, everyone loves these.. 52 | reboot_button = Gtk.Button("Restart now") 53 | reboot_button.get_style_context().add_class("suggested-action") 54 | reboot_button.set_property("margin-top", 10) 55 | reboot_button.set_halign(Gtk.Align.CENTER) 56 | reboot_button.connect('clicked', self.reboot) 57 | box.pack_start(reboot_button, False, False, 0) 58 | 59 | def get_title(self): 60 | return "Installation complete!" 61 | 62 | def get_name(self): 63 | return "complete" 64 | 65 | def get_sidebar_title(self): 66 | return "Complete" 67 | 68 | def get_icon_name(self, plasma=False): 69 | return "start-here-solus" 70 | 71 | def reboot(self, btn, udata=None): 72 | try: 73 | bus = dbus.SystemBus() 74 | rskel = bus.get_object('org.freedesktop.login1', 75 | '/org/freedesktop/login1') 76 | iface = dbus.Interface(rskel, 'org.freedesktop.login1.Manager') 77 | iface.Reboot(True) 78 | except Exception as ex: 79 | print("Exception rebooting: {}".format(ex)) 80 | -------------------------------------------------------------------------------- /os_installer2/pages/disk_location.py: -------------------------------------------------------------------------------- 1 | #!/bin/true 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This file is part of os-installer 5 | # 6 | # Copyright 2013-2020 Solus 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | 14 | from .basepage import BasePage 15 | from gi.repository import Gdk, Gtk, GLib 16 | from os_installer2.diskman import DriveProber 17 | from os_installer2.strategy import DiskStrategyManager 18 | import threading 19 | 20 | 21 | class BrokenWindowsPage(Gtk.Box): 22 | """ Indicate to the user that they booted in the wrong mode """ 23 | 24 | owner = None 25 | 26 | def __init__(self, owner): 27 | Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=0) 28 | 29 | self.owner = owner 30 | 31 | img = Gtk.Image.new_from_icon_name("face-crying-symbolic", 32 | Gtk.IconSize.DIALOG) 33 | 34 | self.pack_start(img, False, False, 10) 35 | 36 | label = Gtk.Label("{}".format( 37 | "You have booted Solus in UEFI mode, however your\n" 38 | "system is configured by Windows to use BIOS mode.\n" 39 | "If you wish to dual-boot, please reboot using the" 40 | " legacy mode.\n" 41 | "Otherwise, you may wipe disks in the next screen")) 42 | label.set_property("xalign", 0.5) 43 | label.set_use_markup(True) 44 | self.pack_start(label, False, False, 10) 45 | 46 | button = Gtk.Button.new_with_label("Continue") 47 | button.get_style_context().add_class("destructive-action") 48 | button.set_halign(Gtk.Align.CENTER) 49 | self.pack_start(button, False, False, 10) 50 | 51 | button.connect("clicked", self.on_clicked) 52 | 53 | self.set_valign(Gtk.Align.CENTER) 54 | self.set_halign(Gtk.Align.CENTER) 55 | 56 | def on_clicked(self, btn, w=None): 57 | """ Update owner page """ 58 | self.owner.can_continue = True 59 | self.owner.info.owner.set_can_next(True) 60 | self.owner.stack.set_visible_child_name("chooser") 61 | 62 | 63 | class ChooserPage(Gtk.Box): 64 | """ Main chooser UI """ 65 | 66 | combo = None 67 | strategy_box = None 68 | respond = False 69 | manager = None 70 | drives = None 71 | 72 | # To record the strategy. 73 | info = None 74 | 75 | def __init__(self): 76 | Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=0) 77 | 78 | self.set_property("orientation", Gtk.Orientation.VERTICAL) 79 | self.set_border_width(40) 80 | 81 | # set up the disk selector 82 | self.combo = Gtk.ComboBoxText() 83 | 84 | self.pack_start(self.combo, False, False, 0) 85 | 86 | self.strategy_box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) 87 | self.strategy_box.set_margin_top(20) 88 | self.pack_start(self.strategy_box, True, True, 0) 89 | 90 | self.respond = True 91 | self.combo.connect("changed", self.on_combo_changed) 92 | 93 | def on_combo_changed(self, combo, w=None): 94 | if not self.respond: 95 | return 96 | drive = self.drives[combo.get_active_id()] 97 | strats = self.manager.get_strategies(drive) 98 | 99 | self.reset_options() 100 | leader = None 101 | for strat in strats: 102 | # update it 103 | strat.update_operations(self.info.owner.get_disk_manager(), 104 | self.info) 105 | button = Gtk.RadioButton.new_with_label_from_widget( 106 | leader, strat.get_display_string()) 107 | button.strategy = strat 108 | if not leader: 109 | leader = button 110 | button.get_child().set_use_markup(True) 111 | button.connect("toggled", self.on_radio_toggle) 112 | self.strategy_box.pack_start(button, False, False, 8) 113 | button.show_all() 114 | # Force selection 115 | if leader: 116 | self.on_radio_toggle(leader) 117 | 118 | def reset_options(self): 119 | """ Reset available strategies """ 120 | for widget in self.strategy_box.get_children(): 121 | widget.destroy() 122 | 123 | def on_radio_toggle(self, radio, w=None): 124 | """ Handle setting of a strategy """ 125 | if not radio.get_active(): 126 | return 127 | strat = radio.strategy 128 | self.info.strategy = strat 129 | 130 | def reset(self): 131 | self.respond = False 132 | self.drives = dict() 133 | self.combo.remove_all() 134 | self.reset_options() 135 | self.respond = True 136 | 137 | def set_drives(self, info, prober): 138 | """ Set the display drives """ 139 | self.info = info 140 | self.info.strategy = None 141 | self.reset() 142 | 143 | self.manager = DiskStrategyManager(prober) 144 | active_id = None 145 | for drive in prober.drives: 146 | self.combo.append(drive.path, drive.get_display_string()) 147 | self.drives[drive.path] = drive 148 | if not active_id: 149 | active_id = drive.path 150 | self.combo.set_active_id(active_id) 151 | 152 | 153 | class WhoopsPage(Gtk.Box): 154 | """ No disks on this system """ 155 | 156 | def __init__(self): 157 | Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=0) 158 | 159 | img = Gtk.Image.new_from_icon_name("face-crying-symbolic", 160 | Gtk.IconSize.DIALOG) 161 | 162 | self.pack_start(img, False, False, 10) 163 | 164 | label = Gtk.Label("{}".format( 165 | "Oh no! Your system has no usable disks available.\n" 166 | "There is nowhere to install Solus.\n" 167 | "Please ensure you have a minimum of 10GB available" 168 | "\nstorage to complete a full Solus installation.")) 169 | label.set_property("xalign", 0.5) 170 | label.set_use_markup(True) 171 | self.pack_start(label, False, False, 10) 172 | 173 | self.set_valign(Gtk.Align.CENTER) 174 | self.set_halign(Gtk.Align.CENTER) 175 | 176 | 177 | class LoadingPage(Gtk.Box): 178 | """ Spinner/load box """ 179 | 180 | def __init__(self): 181 | Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=0) 182 | 183 | self.spinner = Gtk.Spinner() 184 | self.pack_start(self.spinner, False, False, 10) 185 | 186 | self.label = Gtk.Label("Examining local storage devices" + u"…") 187 | self.pack_start(self.label, False, False, 10) 188 | 189 | self.set_valign(Gtk.Align.CENTER) 190 | self.set_halign(Gtk.Align.CENTER) 191 | 192 | def start(self): 193 | self.spinner.start() 194 | 195 | def stop(self): 196 | self.spinner.stop() 197 | 198 | 199 | class InstallerDiskLocationPage(BasePage): 200 | """ Disk location selection. """ 201 | 202 | had_init = False 203 | spinner = None 204 | 205 | stack = None 206 | whoops = None 207 | chooser = None 208 | prober = None 209 | can_continue = False 210 | 211 | def __init__(self): 212 | BasePage.__init__(self) 213 | 214 | self.stack = Gtk.Stack() 215 | self.pack_start(self.stack, True, True, 0) 216 | 217 | self.spinner = LoadingPage() 218 | 219 | self.whoops = WhoopsPage() 220 | self.stack.add_named(self.whoops, "whoops") 221 | broken = BrokenWindowsPage(self) 222 | self.stack.add_named(broken, "broken-windows") 223 | self.stack.add_named(self.spinner, "loading") 224 | self.chooser = ChooserPage() 225 | self.stack.add_named(self.chooser, "chooser") 226 | 227 | self.stack.set_visible_child_name("loading") 228 | 229 | def get_title(self): 230 | return "Where should we install?" 231 | 232 | def get_sidebar_title(self): 233 | return "Disks" 234 | 235 | def get_name(self): 236 | return "disk-location" 237 | 238 | def get_icon_name(self, plasma=False): 239 | if plasma: 240 | return "drive-harddisk" 241 | return "disk-utility" 242 | 243 | def load_disks(self): 244 | """ Load the disks within a thread """ 245 | # Scan parts 246 | dm = self.info.owner.get_disk_manager() 247 | perms = self.info.owner.get_perms_manager() 248 | 249 | perms.up_permissions() 250 | self.prober = DriveProber(dm) 251 | self.info.prober = self.prober 252 | self.prober.probe() 253 | perms.down_permissions() 254 | 255 | # Currently the only GTK call here 256 | Gdk.threads_enter() 257 | self.info.owner.set_can_previous(True) 258 | can_continue = True 259 | 260 | if len(self.prober.drives) == 0: 261 | # No drives 262 | self.stack.set_visible_child_name("whoops") 263 | can_continue = False 264 | elif self.prober.is_broken_windows_uefi(): 265 | self.stack.set_visible_child_name("broken-windows") 266 | self.can_continue = False 267 | else: 268 | # Let them choose 269 | self.stack.set_visible_child_name("chooser") 270 | self.can_continue = True 271 | self.spinner.stop() 272 | Gdk.threads_leave() 273 | 274 | if can_continue: 275 | GLib.idle_add(self.update_disks) 276 | 277 | def update_disks(self): 278 | """ Thread load finished, update UI from discovered info """ 279 | self.chooser.set_drives(self.info, self.prober) 280 | self.info.windows_present = False 281 | for drive in self.prober.drives: 282 | for os in drive.operating_systems: 283 | os_ = drive.operating_systems[os] 284 | if os_.otype == "windows": 285 | self.info.windows_present = True 286 | break 287 | 288 | # Allow forward navigation now 289 | self.info.owner.set_can_next(self.can_continue) 290 | return False 291 | 292 | def init_view(self): 293 | """ Prepare for viewing... """ 294 | if self.had_init and not self.info.invalidated: 295 | return 296 | self.info.invalidated = False 297 | self.can_continue = False 298 | self.had_init = True 299 | self.stack.set_visible_child_name("loading") 300 | self.spinner.start() 301 | self.spinner.show_all() 302 | GLib.idle_add(self.prepare_view) 303 | 304 | def prepare_view(self): 305 | """ Do the real job after GTK has done things.. """ 306 | self.info.owner.set_can_previous(False) 307 | self.queue_draw() 308 | 309 | t = threading.Thread(target=self.load_disks) 310 | t.daemon = True 311 | t.start() 312 | return False 313 | 314 | def prepare(self, info): 315 | self.info = info 316 | self.init_view() 317 | self.info.owner.set_can_next(self.can_continue) 318 | -------------------------------------------------------------------------------- /os_installer2/pages/geoip.py: -------------------------------------------------------------------------------- 1 | #!/bin/true 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This file is part of os-installer 5 | # 6 | # Copyright 2013-2020 Solus 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | 14 | from .basepage import BasePage 15 | from gi.repository import GLib, Gtk 16 | from io import BytesIO 17 | import threading 18 | import re 19 | import pygeoip 20 | import pycurl 21 | 22 | IP_CHECK = "https://location.getsol.us" 23 | TIMEOUT = 10 24 | 25 | 26 | class InstallerGeoipPage(BasePage): 27 | """ Geoip lookup. """ 28 | 29 | info = None 30 | tried_find = False 31 | spinner = None 32 | dlabel = None 33 | 34 | def __init__(self): 35 | BasePage.__init__(self) 36 | 37 | hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) 38 | self.pack_start(hbox, True, True, 0) 39 | hbox.set_margin_top(20) 40 | hbox.set_border_width(40) 41 | 42 | self.spinner = Gtk.Spinner() 43 | hbox.pack_start(self.spinner, False, False, 10) 44 | 45 | self.dlabel = Gtk.Label("Finding your location" + u"…" + "") 46 | self.dlabel.set_use_markup(True) 47 | self.dlabel.set_halign(Gtk.Align.START) 48 | hbox.pack_start(self.dlabel, False, False, 10) 49 | 50 | hbox.set_halign(Gtk.Align.CENTER) 51 | 52 | def get_title(self): 53 | return "Looking for your location" + u"…" 54 | 55 | def get_name(self): 56 | return "geoip-lookup" 57 | 58 | def get_sidebar_title(self): 59 | return u"↠" + " Find location" 60 | 61 | def get_icon_name(self, plasma=False): 62 | return "mark-location-symbolic" 63 | 64 | def prepare(self, info): 65 | self.info = info 66 | if not self.info.enable_geoip: 67 | self.info.owner.skip_page() 68 | return 69 | if self.info.cached_timezone: 70 | self.info.owner.skip_page() 71 | return 72 | if self.tried_find: 73 | return 74 | 75 | # Start our geoip thread 76 | self.schedule_lookup() 77 | 78 | def schedule_lookup(self): 79 | self.tried_find = True 80 | self.info.owner.set_can_next(False) 81 | self.info.owner.set_can_previous(False) 82 | self.spinner.start() 83 | GLib.idle_add(self.begin_thread) 84 | 85 | def begin_thread(self): 86 | t = threading.Thread(target=self.perform_lookup) 87 | t.start() 88 | return False 89 | 90 | def end_thread(self): 91 | if self.info.cached_location: 92 | l = self.info.cached_location 93 | t = self.info.cached_timezone 94 | c = "{} {}".format(l, t) 95 | self.dlabel.set_markup("Found location: {}".format(c)) 96 | else: 97 | self.dlabel.set_markup("Unable to find location") 98 | self.info.owner.set_can_previous(True) 99 | self.spinner.stop() 100 | 101 | GLib.timeout_add(1500, self.go_skipping) 102 | return False 103 | 104 | def go_skipping(self): 105 | self.info.owner.skip_page() 106 | return False 107 | 108 | def get_ip_address(self): 109 | """ Get our external IP address for this machine """ 110 | try: 111 | buffer = BytesIO() 112 | curl = pycurl.Curl() 113 | curl.setopt(curl.IPRESOLVE, 1) 114 | curl.setopt(curl.WRITEDATA, buffer) 115 | curl.setopt(curl.URL, IP_CHECK) 116 | curl.perform() 117 | curl.close() 118 | b = buffer.getvalue() 119 | contents = b.decode('utf-8') 120 | 121 | regex = r'Address: (.+)$' 122 | reg = re.compile(regex) 123 | return reg.search(contents).group(1) 124 | except Exception as e: 125 | print(e) 126 | return None 127 | 128 | def perform_lookup(self): 129 | self.info.owner.get_perms_manager().down_permissions() 130 | """ Perform the actual lookup """ 131 | ip = self.get_ip_address() 132 | if not ip: 133 | # Consider getting something useful here... 134 | GLib.idle_add(self.end_thread) 135 | return 136 | 137 | gi = pygeoip.GeoIP("/usr/share/GeoIP/City.dat") 138 | country = gi.country_code_by_addr(ip) 139 | timezone = gi.time_zone_by_addr(ip) 140 | self.info.cached_location = country 141 | self.info.cached_timezone = timezone 142 | # Return to thread main 143 | GLib.idle_add(self.end_thread) 144 | -------------------------------------------------------------------------------- /os_installer2/pages/keyboard.py: -------------------------------------------------------------------------------- 1 | #!/bin/true 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This file is part of os-installer 5 | # 6 | # Copyright 2013-2020 Solus 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | 14 | from .basepage import BasePage 15 | from gi.repository import Gtk, GnomeDesktop 16 | import subprocess 17 | 18 | 19 | class KbLabel(Gtk.Box): 20 | """ View label for locales, save code duping """ 21 | 22 | kb = None 23 | dname = None 24 | 25 | def __init__(self, kb, info): 26 | Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, spacing=0) 27 | 28 | self.kb = kb 29 | 30 | lab = Gtk.Label("") 31 | lab.set_halign(Gtk.Align.START) 32 | 33 | self.dname = info[1] 34 | self.sname = info[2] 35 | self.layout = info[3] 36 | self.variant = info[4] 37 | 38 | self.language = info[2] 39 | self.country = info[3] 40 | 41 | self.extras = info[4] 42 | 43 | self.set_property("margin", 10) 44 | 45 | lab.set_text(self.dname) 46 | self.pack_start(lab, True, True, 0) 47 | 48 | self.show_all() 49 | 50 | 51 | class InstallerKeyboardPage(BasePage): 52 | """ Basic location detection page. """ 53 | 54 | layouts = None 55 | info = None 56 | had_init = False 57 | xkb = None 58 | shown_layouts = None 59 | moar_button = None 60 | extras = None 61 | 62 | def __init__(self): 63 | BasePage.__init__(self) 64 | 65 | # Hold everything up in a grid 66 | grid = Gtk.Grid() 67 | self.pack_start(grid, True, True, 0) 68 | grid.set_column_spacing(6) 69 | grid.set_row_spacing(6) 70 | grid.set_margin_start(32) 71 | grid.set_margin_top(40) 72 | grid.set_halign(Gtk.Align.CENTER) 73 | 74 | # Init main layouts view 75 | self.layouts = Gtk.ListBox() 76 | scroll = Gtk.ScrolledWindow(None, None) 77 | scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) 78 | scroll.add(self.layouts) 79 | scroll.set_shadow_type(Gtk.ShadowType.ETCHED_IN) 80 | scroll.set_vexpand(True) 81 | grid.attach(scroll, 0, 0, 2, 1) 82 | 83 | self.layouts.set_size_request(500, -1) 84 | 85 | # Input tester 86 | inp_entry = Gtk.Entry() 87 | t_str = "Type here to test your keyboard layout" 88 | inp_entry.set_placeholder_text(t_str) 89 | 90 | grid.attach(inp_entry, 0, 1, 2, 1) 91 | 92 | self.moar_button = Gtk.Image.new_from_icon_name("view-more-symbolic", 93 | Gtk.IconSize.MENU) 94 | self.moar_button.set_property("margin", 8) 95 | 96 | self.layouts.connect_after("row-selected", self.on_row_select) 97 | 98 | def on_row_select(self, lbox, newrb=None): 99 | """ Handle selections of locales """ 100 | self.info.keyboard = None 101 | self.info.keyboard_sz = None 102 | if not newrb: 103 | self.info.keyboard = None 104 | self.info.keyboard_sz = None 105 | self.info.owner.set_can_next(False) 106 | return 107 | child = newrb.get_child() 108 | if child == self.moar_button: 109 | self.init_remaining() 110 | return 111 | self.info.keyboard = child.kb 112 | self.info.keyboard_sz = child.dname 113 | try: 114 | subprocess.check_call("setxkbmap {} {}".format(child.layout,child.variant), shell=True) 115 | except Exception as e: 116 | print("@ERR@: Couldn\'t set the keyboard layout: {}".format(e)) 117 | 118 | self.info.owner.set_can_next(True) 119 | 120 | def init_view(self): 121 | """ Initialise ourself from GNOME XKB """ 122 | if self.had_init: 123 | return 124 | self.had_init = True 125 | self.xkb = GnomeDesktop.XkbInfo() 126 | 127 | items = GnomeDesktop.parse_locale(self.info.locale) 128 | if items[0]: 129 | lang = items[1] 130 | country = items[2] 131 | else: 132 | # Shouldn't ever happen, but ya never know. 133 | lang = "en" 134 | country = "US" 135 | 136 | if self.info.cached_location: 137 | country = self.info.cached_location.upper() 138 | 139 | l = self.info.locale 140 | success, type_, id_ = GnomeDesktop.get_input_source_from_locale(l) 141 | 142 | kbset = set() 143 | kbset.update(self.xkb.get_layouts_for_country(country)) 144 | kbset.update(self.xkb.get_layouts_for_language(lang)) 145 | 146 | major_layouts = self.xkb.get_all_layouts() 147 | for item in major_layouts: 148 | xkbinf = self.xkb.get_layout_info(item) 149 | if not xkbinf[0]: 150 | continue 151 | if xkbinf[3].lower() == country.lower(): 152 | kbset.add(item) 153 | 154 | layouts = list() 155 | for x in kbset: 156 | info = self.xkb.get_layout_info(x) 157 | if not info[0]: 158 | continue 159 | widget = KbLabel(x, info) 160 | layouts.append(widget) 161 | 162 | c = country.lower() 163 | native = filter(lambda x: x.country.lower() == c, layouts) 164 | 165 | primary = None 166 | 167 | if not native: 168 | native = layouts 169 | for item in native: 170 | if item.layout[:2].lower() == lang.lower() and not item.extras: 171 | primary = item 172 | else: 173 | for item in native: 174 | if not item.extras: 175 | primary = item 176 | break 177 | 178 | self.added = 0 179 | self.extras = list() 180 | 181 | def append_inner(layout, item): 182 | if layout in self.shown_layouts: 183 | return 184 | if self.added >= 5: 185 | self.extras.append(item) 186 | return 187 | self.shown_layouts.add(layout) 188 | self.layouts.add(item) 189 | self.added += 1 190 | 191 | self.shown_layouts = set() 192 | if primary: 193 | append_inner(primary.kb, primary) 194 | for item in native: 195 | append_inner(item.kb, item) 196 | for item in layouts: 197 | append_inner(item.kb, item) 198 | 199 | self.moar_button.show_all() 200 | kids = self.layouts.get_children() 201 | if kids: 202 | s = self.layouts.get_children()[0] 203 | self.layouts.select_row(s) 204 | 205 | self.layouts.add(self.moar_button) 206 | 207 | def init_remaining(self): 208 | layouts = self.xkb.get_all_layouts() 209 | 210 | self.moar_button.get_parent().hide() 211 | 212 | appends = list() 213 | # Deal with extras first 214 | self.extras = sorted(self.extras, key=lambda x: x.dname) 215 | for item in self.extras: 216 | if item.kb in self.shown_layouts: 217 | continue 218 | self.shown_layouts.add(item.kb) 219 | self.layouts.add(item) 220 | 221 | for layout in layouts: 222 | # Don't dupe 223 | if layout in self.shown_layouts: 224 | continue 225 | info = self.xkb.get_layout_info(layout) 226 | success = info[0] 227 | if not success: 228 | continue 229 | 230 | widget = KbLabel(layout, info) 231 | appends.append(widget) 232 | appends.sort(key=lambda x: x.dname.lower()) 233 | for app in appends: 234 | if app.kb in self.shown_layouts: 235 | continue 236 | self.layouts.add(app) 237 | 238 | def get_title(self): 239 | return "Choose a keyboard layout" 240 | 241 | def get_sidebar_title(self): 242 | return "Keyboard" 243 | 244 | def get_name(self): 245 | return "keyboard" 246 | 247 | def get_icon_name(self, plasma=False): 248 | if plasma: 249 | return "input-keyboard" 250 | # Just looks nicer than input-keyboard outside breeze 251 | return "preferences-desktop-keyboard-shortcuts" 252 | 253 | def prepare(self, info): 254 | self.info = info 255 | self.init_view() 256 | if self.info.keyboard: 257 | self.info.owner.set_can_next(True) 258 | else: 259 | self.info.owner.set_can_next(False) 260 | -------------------------------------------------------------------------------- /os_installer2/pages/language.py: -------------------------------------------------------------------------------- 1 | #!/bin/true 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This file is part of os-installer 5 | # 6 | # Copyright 2013-2020 Solus 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | 14 | from .basepage import BasePage 15 | from . import default_locales 16 | from gi.repository import Gtk, GnomeDesktop, Gdk 17 | 18 | 19 | class LcLabel(Gtk.Label): 20 | """ View label for locales, save code duping """ 21 | 22 | lc_code = None 23 | untransl = None 24 | 25 | def __init__(self, lc_code): 26 | Gtk.Label.__init__(self) 27 | self.set_text(lc_code) 28 | self.set_halign(Gtk.Align.START) 29 | self.lc_code = lc_code 30 | 31 | # transl = GnomeDesktop.get_language_from_locale(lc_code, lc_code) 32 | untransl = GnomeDesktop.get_language_from_locale(lc_code, None) 33 | self.set_property("margin", 8) 34 | 35 | self.dname = untransl 36 | 37 | self.set_text(untransl) 38 | 39 | self.show() 40 | 41 | 42 | class InstallerLanguagePage(BasePage): 43 | """ Basic language page. """ 44 | 45 | # Scrollbox 46 | scroll = None 47 | 48 | # Main content 49 | listbox = None 50 | moar_button = None 51 | 52 | info = None 53 | 54 | def __init__(self): 55 | BasePage.__init__(self) 56 | 57 | self.scroll = Gtk.ScrolledWindow(None, None) 58 | self.scroll.set_shadow_type(Gtk.ShadowType.ETCHED_IN) 59 | self.scroll.set_border_width(40) 60 | self.add(self.scroll) 61 | 62 | self.listbox = Gtk.ListBox() 63 | self.scroll.add(self.listbox) 64 | self.scroll.set_halign(Gtk.Align.CENTER) 65 | self.listbox.set_size_request(500, -1) 66 | 67 | # Fix up policy 68 | self.scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER) 69 | 70 | self.moar_button = Gtk.Image.new_from_icon_name("view-more-symbolic", 71 | Gtk.IconSize.MENU) 72 | self.moar_button.set_property("margin", 8) 73 | self.moar_button.show_all() 74 | self.listbox.connect_after("row-selected", self.on_row_select) 75 | 76 | def on_row_select(self, lbox, newrb=None): 77 | """ Handle selections of locales """ 78 | self.info.locale = None 79 | self.info.locale_sz = None 80 | if not newrb: 81 | self.info.owner.set_can_next(False) 82 | return 83 | child = newrb.get_child() 84 | if child == self.moar_button: 85 | self.init_remaining() 86 | return 87 | self.info.locale = child.lc_code 88 | self.info.locale_sz = child.dname 89 | self.info.owner.set_can_next(True) 90 | 91 | def do_expensive_init(self): 92 | """ Do the hard work of actually setting up the view """ 93 | Gdk.threads_enter() 94 | for lc in default_locales: 95 | self.listbox.add(LcLabel(lc)) 96 | self.listbox.add(self.moar_button) 97 | Gdk.threads_leave() 98 | 99 | def init_remaining(self): 100 | """ Add the rest to the list """ 101 | self.scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) 102 | self.scroll.set_vexpand(True) 103 | self.scroll.set_valign(Gtk.Align.FILL) 104 | locales = GnomeDesktop.get_all_locales() 105 | self.moar_button.get_parent().hide() 106 | appends = list() 107 | for lc in locales: 108 | if lc in default_locales: 109 | continue 110 | item = LcLabel(lc) 111 | appends.append(item) 112 | appends.sort(key=lambda x: x.dname.lower()) 113 | for item in appends: 114 | self.listbox.add(item) 115 | 116 | def prepare(self, info): 117 | # Nothing to seed with. 118 | self.info = info 119 | if self.info.locale: 120 | self.info.owner.set_can_next(True) 121 | else: 122 | self.info.owner.set_can_next(False) 123 | 124 | def get_title(self): 125 | return "Choose a language" 126 | 127 | def get_sidebar_title(self): 128 | return "Language" 129 | 130 | def get_name(self): 131 | return "language" 132 | 133 | def get_icon_name(self, plasma=False): 134 | return "preferences-desktop-locale" 135 | -------------------------------------------------------------------------------- /os_installer2/pages/location.py: -------------------------------------------------------------------------------- 1 | #!/bin/true 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This file is part of os-installer 5 | # 6 | # Copyright 2013-2020 Solus 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | 14 | from .basepage import BasePage 15 | from gi.repository import Gtk 16 | 17 | 18 | class InstallerLocationPage(BasePage): 19 | """ Basic location detection page. """ 20 | 21 | info = None 22 | checkbox = None 23 | 24 | def __init__(self): 25 | BasePage.__init__(self) 26 | 27 | lab = Gtk.Label("The next few questions relate to your location. To \ 28 | speed things up, the installer can perform a quick check to detect where you \ 29 | are in the world and proceed automatically.") 30 | lab.set_property("xalign", 0.0) 31 | lab.set_margin_top(40) 32 | 33 | lab.set_margin_start(32) 34 | 35 | lab.set_line_wrap(True) 36 | self.pack_start(lab, False, False, 0) 37 | 38 | check_str = "Find my location automatically." 39 | self.checkbox = Gtk.CheckButton.new_with_label(check_str) 40 | self.pack_start(self.checkbox, False, False, 0) 41 | self.checkbox.set_margin_top(40) 42 | self.checkbox.set_margin_start(32) 43 | 44 | self.checkbox.connect("toggled", self.on_toggled) 45 | 46 | def on_toggled(self, w, d=None): 47 | if not self.info: 48 | return 49 | self.info.enable_geoip = w.get_active() 50 | 51 | def get_title(self): 52 | return "Where are you?" 53 | 54 | def get_sidebar_title(self): 55 | return "Location" 56 | 57 | def get_name(self): 58 | return "location" 59 | 60 | def get_icon_name(self, plasma=False): 61 | if plasma: 62 | return "applications-internet" 63 | return "maps" 64 | 65 | def prepare(self, info): 66 | self.info = info 67 | if self.info.cached_timezone: 68 | self.checkbox.set_sensitive(False) 69 | self.checkbox.set_tooltip_text("Location already found") 70 | -------------------------------------------------------------------------------- /os_installer2/pages/summary.py: -------------------------------------------------------------------------------- 1 | #!/bin/true 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This file is part of os-installer 5 | # 6 | # Copyright 2013-2020 Solus 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | 14 | from .basepage import BasePage 15 | from gi.repository import Gtk 16 | 17 | 18 | class FramedHeader(Gtk.Frame): 19 | """ Summary header widget """ 20 | 21 | vbox = None 22 | 23 | def __init__(self, icon_name, title): 24 | Gtk.Frame.__init__(self) 25 | 26 | box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) 27 | 28 | sz = Gtk.IconSize.DIALOG 29 | image = Gtk.Image.new_from_icon_name(icon_name, sz) 30 | box.pack_start(image, False, False, 0) 31 | 32 | image.set_property("margin", 10) 33 | image.set_valign(Gtk.Align.START) 34 | 35 | label = Gtk.Label("{}".format(title)) 36 | label.set_use_markup(True) 37 | box.pack_start(label, False, False, 0) 38 | label.set_halign(Gtk.Align.START) 39 | label.set_valign(Gtk.Align.START) 40 | label.set_property("margin", 10) 41 | 42 | self.vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) 43 | box.pack_end(self.vbox, False, False, 0) 44 | self.vbox.set_property("margin", 10) 45 | 46 | self.add(box) 47 | 48 | def add_label(self, label): 49 | self.vbox.pack_start(label, False, False, 2) 50 | label.show_all() 51 | 52 | 53 | class InstallerSummaryPage(BasePage): 54 | """ Installer summary page. """ 55 | 56 | locale_details = None 57 | install_details = None 58 | system_details = None 59 | user_details = None 60 | 61 | def __init__(self, plasma): 62 | BasePage.__init__(self) 63 | self.plasma = plasma 64 | 65 | scroll = Gtk.ScrolledWindow(None, None) 66 | scroll.set_border_width(40) 67 | scroll.set_margin_top(20) 68 | self.pack_start(scroll, True, True, 0) 69 | scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) 70 | 71 | items = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) 72 | scroll.add(items) 73 | scroll.set_overlay_scrolling(False) 74 | self.locale_details = FramedHeader( 75 | "preferences-desktop-locale", 76 | "Language & Region") 77 | items.pack_start(self.locale_details, False, False, 2) 78 | 79 | disk_icon = "disk-utility" 80 | if self.plasma: 81 | disk_icon = "drive-harddisk" 82 | 83 | self.install_details = FramedHeader(disk_icon, 84 | "Installation") 85 | items.pack_start(self.install_details, False, False, 2) 86 | 87 | self.user_details = FramedHeader("system-users", "Users") 88 | items.pack_start(self.user_details, False, False, 2) 89 | 90 | self.system_details = FramedHeader("preferences-system", 91 | "System Details") 92 | items.pack_start(self.system_details, False, False, 2) 93 | 94 | def get_title(self): 95 | return "Review options before installation" 96 | 97 | def get_sidebar_title(self): 98 | return "Summary" 99 | 100 | def get_name(self): 101 | return "summary" 102 | 103 | def get_icon_name(self, plasma=False): 104 | if plasma: 105 | return "korg-todo" 106 | return "gnome-todo" 107 | 108 | def _clean_label(self, label): 109 | lab = Gtk.Label(label) 110 | lab.set_halign(Gtk.Align.END) 111 | return lab 112 | 113 | def prepare(self, info): 114 | info.owner.set_final_step(True) 115 | 116 | # First up, region stuff 117 | for kid in self.locale_details.vbox.get_children(): 118 | kid.destroy() 119 | 120 | self.locale_details.add_label(self._clean_label( 121 | "Use {} as system language".format(info.locale_sz))) 122 | self.locale_details.add_label(self._clean_label( 123 | "Use {} as default keyboard layout".format(info.keyboard_sz))) 124 | self.locale_details.add_label(self._clean_label( 125 | "Set timezone to {}".format(info.timezone))) 126 | 127 | # Details 128 | for kid in self.system_details.vbox.get_children(): 129 | kid.destroy() 130 | self.system_details.add_label(self._clean_label( 131 | "Set device host-name to \"{}\"".format(info.hostname))) 132 | if info.bootloader_install: 133 | if info.bootloader_sz == "c": 134 | s = info.bootloader 135 | else: 136 | s = "Install bootloader to {}".format(info.bootloader_sz) 137 | self.system_details.add_label(self._clean_label(s)) 138 | 139 | # Users 140 | for kid in self.user_details.vbox.get_children(): 141 | kid.destroy() 142 | for user in info.users: 143 | sz = "Create administrative user {} ({})" 144 | if not user.admin: 145 | sz = "Create regular user {} ({})" 146 | self.user_details.add_label(self._clean_label( 147 | sz.format(user.realname, user.username))) 148 | 149 | # disk 150 | for kid in self.install_details.vbox.get_children(): 151 | kid.destroy() 152 | info.strategy.reset_operations() 153 | info.strategy.update_operations(info.owner.get_disk_manager(), info) 154 | for line in info.strategy.explain(info.owner.get_disk_manager(), info): 155 | self.install_details.add_label(self._clean_label(line)) 156 | -------------------------------------------------------------------------------- /os_installer2/pages/system.py: -------------------------------------------------------------------------------- 1 | #!/bin/true 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This file is part of os-installer 5 | # 6 | # Copyright 2013-2020 Solus 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | 14 | from .basepage import BasePage 15 | from gi.repository import Gtk 16 | import re 17 | 18 | # This is all we allow. OK. 19 | ValidHostnameRegex = "^[a-z_][a-z0-9_-]*[$]?$" 20 | 21 | 22 | class InstallerSystemPage(BasePage): 23 | """ System Settings page. """ 24 | 25 | info = None 26 | host_reg = None 27 | host_entry = None 28 | error_label = None 29 | check_boot = None 30 | combo_boot = None 31 | # Bootloader issues 32 | error_label2 = None 33 | respond = True 34 | 35 | def __init__(self): 36 | BasePage.__init__(self) 37 | self.host_reg = re.compile(ValidHostnameRegex) 38 | 39 | wid_group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL) 40 | 41 | mbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) 42 | mbox.set_margin_top(40) 43 | self.pack_start(mbox, False, False, 0) 44 | mbox.set_halign(Gtk.Align.CENTER) 45 | mbox.set_hexpand(False) 46 | mbox.set_vexpand(False) 47 | 48 | # hostname section 49 | host = Gtk.Frame() 50 | host.set_shadow_type(Gtk.ShadowType.NONE) 51 | host.set_label("What name should this computer use on the network?") 52 | host.get_label_widget().set_margin_bottom(8) 53 | mbox.pack_start(host, False, False, 10) 54 | 55 | self.host_entry = Gtk.Entry() 56 | self.host_entry.connect("changed", self.host_validate) 57 | self.host_entry.set_placeholder_text("Type the hostname here") 58 | host.add(self.host_entry) 59 | 60 | wid_group.add_widget(host) 61 | 62 | boot = Gtk.Frame() 63 | self.boot_frame = boot 64 | boot.set_halign(Gtk.Align.CENTER) 65 | boot.set_shadow_type(Gtk.ShadowType.NONE) 66 | self.check_boot = Gtk.CheckButton.new_with_label( 67 | "Install a bootloader") 68 | self.check_boot.set_active(True) 69 | self.check_boot.connect("toggled", self.on_boot_toggled) 70 | self.check_boot.set_margin_bottom(5) 71 | self.check_boot.set_margin_top(5) 72 | boot.set_label_widget(self.check_boot) 73 | self.combo_boot = Gtk.ComboBoxText() 74 | self.combo_boot.connect("changed", self.on_combo_changed) 75 | boot.add(self.combo_boot) 76 | self.pack_start(boot, False, False, 0) 77 | wid_group.add_widget(self.combo_boot) 78 | 79 | self.error_label = Gtk.Label.new("") 80 | self.error_label.set_valign(Gtk.Align.START) 81 | self.pack_end(self.error_label, False, False, 0) 82 | wid_group.add_widget(self.error_label) 83 | 84 | self.error_label2 = Gtk.Label.new("") 85 | self.error_label2.set_valign(Gtk.Align.START) 86 | self.pack_end(self.error_label2, False, False, 0) 87 | wid_group.add_widget(self.error_label2) 88 | 89 | def on_boot_toggled(self, w, d=None): 90 | """ Handle bootloader install """ 91 | self.combo_boot.set_sensitive(w.get_active()) 92 | self.info.bootloader_install = w.get_active() 93 | 94 | def on_combo_changed(self, combo, w=None): 95 | """ Combo updated """ 96 | if not self.respond: 97 | return 98 | if not self.info: 99 | return 100 | self.info.bootloader_sz = combo.get_active_id() 101 | self.info.bootloader = combo.get_active_text() 102 | 103 | self.check_forward() 104 | 105 | def check_forward(self): 106 | """ Determine if we can forward/back """ 107 | bs = [self.info.hostname] 108 | if self.check_boot.get_active(): 109 | bs.append(self.info.bootloader) 110 | misfires = [x for x in bs if not x] 111 | if len(misfires) == 0: 112 | self.info.owner.set_can_next(True) 113 | else: 114 | self.info.owner.set_can_next(False) 115 | 116 | def host_validate(self, entry): 117 | """ Validate the hostname """ 118 | text = entry.get_text() 119 | match = self.host_reg.match(text) 120 | can_fwd = False 121 | if match is None: 122 | entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, 123 | "action-unavailable-symbolic") 124 | self.error_label.set_markup( 125 | "Hostnames must be lowercase, and only contain " 126 | "letters,\nnumbers, hyphens and underscores." 127 | "Hostnames must\nalso start with a lowercase letter.") 128 | else: 129 | entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, 130 | "emblem-ok-symbolic") 131 | self.error_label.set_markup("") 132 | can_fwd = True 133 | if not self.info: 134 | return 135 | if can_fwd: 136 | self.info.hostname = text 137 | else: 138 | self.info.hostname = None 139 | self.check_forward() 140 | 141 | def get_title(self): 142 | return "Configure the bootloader & hostname" 143 | 144 | def get_name(self): 145 | return "system-settings" 146 | 147 | def get_sidebar_title(self): 148 | return "System Settings" 149 | 150 | def get_icon_name(self, plasma=False): 151 | return "preferences-system" 152 | 153 | def prepare(self, info): 154 | self.info = info 155 | dm = self.info.owner.get_disk_manager() 156 | if dm.is_efi_booted(): 157 | self.info.bootloader_install = True 158 | self.check_boot.set_active(True) 159 | self.check_boot.set_sensitive(False) 160 | self.check_boot.set_label( 161 | "Bootloader installation mandatory with UEFI") 162 | 163 | self.check_forward() 164 | 165 | self.respond = False 166 | self.combo_boot.remove_all() 167 | options = info.strategy.get_boot_loader_options() 168 | for loader, id in options: 169 | self.combo_boot.append(id, loader) 170 | self.respond = True 171 | if len(options) > 0: 172 | self.combo_boot.set_active(0) 173 | self.error_label2.set_label("") 174 | return 175 | info.bootloader = None 176 | self.info.owner.set_can_next(False) 177 | err = info.strategy.get_errors() 178 | # UEFI specific 179 | if err: 180 | self.error_label2.set_label( 181 | "Failed to find location for bootloader: {}".format(err)) 182 | else: 183 | self.check_boot.set_active(False) 184 | self.error_label2.set_label( 185 | "Warning: Cannot find a valid bootloader location (MBR disk)") 186 | -------------------------------------------------------------------------------- /os_installer2/pages/timezone.py: -------------------------------------------------------------------------------- 1 | #!/bin/true 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This file is part of os-installer 5 | # 6 | # Copyright 2013-2020 Solus 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | 14 | from .basepage import BasePage 15 | from os_installer2.tz import Database 16 | 17 | from gi.repository import TimezoneMap, Gtk, Gdk 18 | 19 | 20 | class InstallerTimezonePage(BasePage): 21 | """ timezone setup page. """ 22 | 23 | tmap = None 24 | locations = None 25 | db = None 26 | 27 | def __init__(self): 28 | BasePage.__init__(self) 29 | self.frame = Gtk.Frame() 30 | self.frame.set_shadow_type(Gtk.ShadowType.NONE) 31 | self.tmap = TimezoneMap.TimezoneMap() 32 | self.pack_start(self.frame, True, True, 0) 33 | self.frame.set_margin_end(0) 34 | self.frame.set_margin_start(0) 35 | self.frame.add(self.tmap) 36 | 37 | self.locations = Gtk.Entry() 38 | self.locations.set_property("margin-right", 30) 39 | self.locations.set_property("margin-start", 30) 40 | self.locations.set_property("margin-top", 10) 41 | self.pack_end(self.locations, False, False, 0) 42 | 43 | self.locations.set_placeholder_text("Search for your timezone" + u"…") 44 | 45 | completion = TimezoneMap.TimezoneCompletion() 46 | completion.set_text_column(0) 47 | completion.set_inline_completion(True) 48 | completion.set_inline_selection(True) 49 | completion.connect("match-selected", self.change_timezone) 50 | self.locations.set_completion(completion) 51 | self.tmap.connect("location-changed", self.changed) 52 | 53 | def do_expensive_init(self): 54 | # Set up timezone database 55 | self.db = Database() 56 | 57 | tz_model = Gtk.ListStore(str, str, str, str, float, float, str) 58 | 59 | for item in self.db.locations: 60 | tz_model.append([item.human_zone, item.human_country, None, 61 | item.country, item.longitude, item.latitude, 62 | item.zone]) 63 | 64 | Gdk.threads_enter() 65 | self.locations.get_completion().set_model(tz_model) 66 | Gdk.threads_leave() 67 | 68 | def get_title(self): 69 | return "Choose your timezone" 70 | 71 | def get_sidebar_title(self): 72 | return "Timezone" 73 | 74 | def get_name(self): 75 | return "timezone" 76 | 77 | def get_icon_name(self, plasma=False): 78 | return "preferences-system-time" 79 | 80 | def change_timezone(self, completion, model, selection): 81 | item = model[selection] 82 | zone = item[6] 83 | self.tmap.set_timezone(zone) 84 | 85 | def changed(self, map, location): 86 | zone = location.get_property("zone") 87 | nice_loc = self.db.tz_to_loc[zone] 88 | 89 | self.timezone_human = "{} ({})".format(nice_loc.human_zone, 90 | nice_loc.human_country) 91 | self.tmap.set_watermark(self.timezone_human) 92 | self.locations.set_text(nice_loc.human_zone) 93 | 94 | # Ok to go forward 95 | self.info.owner.set_can_next(True) 96 | self.info.timezone = zone 97 | self.info.timezone_c = location.get_property("country") 98 | 99 | def prepare(self, info): 100 | self.info = info 101 | if self.info.timezone: 102 | self.info.owner.set_can_next(True) 103 | else: 104 | # Use geoip 105 | if self.info.cached_timezone: 106 | self.tmap.set_timezone(self.info.cached_timezone) 107 | self.timezone = self.info.cached_timezone 108 | self.info.owner.set_can_next(True) 109 | else: 110 | self.info.owner.set_can_next(False) 111 | -------------------------------------------------------------------------------- /os_installer2/pages/users.py: -------------------------------------------------------------------------------- 1 | #!/bin/true 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This file is part of os-installer 5 | # 6 | # Copyright 2013-2020 Solus 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | 14 | from .basepage import BasePage 15 | from gi.repository import Gtk 16 | from os_installer2.users import User, USERNAME_REGEX, PASSWORD_LENGTH 17 | import re 18 | 19 | LABEL_COLUMN = 0 20 | DATA_COLUMN = 1 21 | 22 | 23 | class UserPanel(Gtk.Box): 24 | """Userpanel. Represents a user. Whoda thunk it. """ 25 | 26 | def __init__(self, user): 27 | Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=0) 28 | 29 | self.user = user 30 | 31 | label = Gtk.Label("{} - {}".format( 32 | self.user.realname, 33 | self.user.username)) 34 | label.set_use_markup(True) 35 | label_details = Gtk.Label("") 36 | details = "" 37 | 38 | if self.user.autologin: 39 | details += " - {}".format( 40 | "will be automatically logged into the computer") 41 | else: 42 | details += " - {}".format( 43 | "will use a password to log into the computer") 44 | if self.user.admin: 45 | details += "\n - {}".format( 46 | "will have administrative capabilities") 47 | else: 48 | details += "\n - {}".format("will be an ordinary user") 49 | 50 | label_details.set_markup(details) 51 | label.set_halign(Gtk.Align.START) 52 | label_details.set_halign(Gtk.Align.START) 53 | self.pack_start(label, True, True, 4) 54 | self.pack_start(label_details, False, False, 0) 55 | 56 | 57 | class NewUserPage(Gtk.Grid): 58 | """ Form for creating a new user """ 59 | 60 | uname_field = None 61 | rname_field = None 62 | pword_field = None 63 | pword_field2 = None 64 | 65 | def is_bad_field(self, field): 66 | """ Validation for gecos """ 67 | t = field.get_text() 68 | if len(t) < 2: 69 | return True 70 | bad_guys = ['"', '\'', '/', '\\', ';', '@', '!'] 71 | hits = [x for x in bad_guys if x in t] 72 | if len(hits) > 0: 73 | return True 74 | return False 75 | 76 | def validator(self, entry): 77 | if entry == self.uname_field: 78 | # Perform username validation 79 | if self.username_regex.match(entry.get_text()): 80 | self.uname_field.set_icon_from_icon_name( 81 | Gtk.EntryIconPosition.SECONDARY, "emblem-ok-symbolic") 82 | self.update_score(self.uname_field, True) 83 | else: 84 | # Username = bad 85 | self.uname_field.set_icon_from_icon_name( 86 | Gtk.EntryIconPosition.SECONDARY, 87 | "action-unavailable-symbolic") 88 | self.update_score(self.uname_field, False) 89 | elif entry == self.rname_field: 90 | if not self.is_bad_field(self.rname_field): 91 | self.rname_field.set_icon_from_icon_name( 92 | Gtk.EntryIconPosition.SECONDARY, "emblem-ok-symbolic") 93 | self.update_score(self.rname_field, True) 94 | else: 95 | # Bad realname 96 | self.rname_field.set_icon_from_icon_name( 97 | Gtk.EntryIconPosition.SECONDARY, 98 | "action-unavailable-symbolic") 99 | self.update_score(self.rname_field, False) 100 | else: 101 | # Handle the two password fields together 102 | pass1 = self.pword_field.get_text() 103 | pass2 = self.pword_field2.get_text() 104 | 105 | if len(pass1) >= PASSWORD_LENGTH: 106 | self.pword_field.set_icon_from_icon_name( 107 | Gtk.EntryIconPosition.SECONDARY, "emblem-ok-symbolic") 108 | self.update_score(self.pword_field, True) 109 | else: 110 | # Bad password 111 | self.pword_field.set_icon_from_icon_name( 112 | Gtk.EntryIconPosition.SECONDARY, 113 | "action-unavailable-symbolic") 114 | self.update_score(self.pword_field, False) 115 | 116 | if len(pass1) >= PASSWORD_LENGTH and pass1 == pass2: 117 | self.pword_field2.set_icon_from_icon_name( 118 | Gtk.EntryIconPosition.SECONDARY, "emblem-ok-symbolic") 119 | self.update_score(self.pword_field2, True) 120 | else: 121 | # Bad password2 122 | self.pword_field2.set_icon_from_icon_name( 123 | Gtk.EntryIconPosition.SECONDARY, 124 | "action-unavailable-symbolic") 125 | self.update_score(self.pword_field2, False) 126 | 127 | def update_score(self, widget, score): 128 | """ Update the score for validation """ 129 | if widget not in self.scores: 130 | self.scores[widget] = score 131 | else: 132 | self.scores[widget] = score 133 | 134 | total_score = len([i for i in self.scores.values() if i]) 135 | self.ok.set_sensitive(total_score == self.needed_score) 136 | 137 | def __init__(self, owner): 138 | Gtk.Grid.__init__(self) 139 | self.owner = owner 140 | self.set_margin_top(40) 141 | 142 | self.set_column_spacing(10) 143 | self.set_row_spacing(10) 144 | self.set_margin_left(100) 145 | self.set_margin_right(100) 146 | 147 | self.scores = dict() 148 | self.needed_score = 4 149 | 150 | self.username_regex = re.compile(USERNAME_REGEX) 151 | 152 | row = 0 153 | uname_label = Gtk.Label("Username:") 154 | self.uname_field = Gtk.Entry() 155 | self.uname_field.set_hexpand(True) 156 | self.uname_field.connect("changed", self.validator) 157 | self.attach(uname_label, LABEL_COLUMN, row, 1, 1) 158 | self.attach(self.uname_field, DATA_COLUMN, row, 1, 1) 159 | 160 | row += 1 161 | rname_label = Gtk.Label("Real name:") 162 | self.rname_field = Gtk.Entry() 163 | self.rname_field.connect("changed", self.validator) 164 | self.attach(rname_label, LABEL_COLUMN, row, 1, 1) 165 | self.attach(self.rname_field, DATA_COLUMN, row, 1, 1) 166 | 167 | row += 1 168 | pword_label = Gtk.Label("Password:") 169 | self.pword_field = Gtk.Entry() 170 | self.pword_field.set_visibility(False) 171 | self.pword_field.connect("changed", self.validator) 172 | self.attach(pword_label, LABEL_COLUMN, row, 1, 1) 173 | self.attach(self.pword_field, DATA_COLUMN, row, 1, 1) 174 | 175 | row += 1 176 | pword_label2 = Gtk.Label("Confirm password:") 177 | self.pword_field2 = Gtk.Entry() 178 | self.pword_field2.connect("changed", self.validator) 179 | self.pword_field2.set_visibility(False) 180 | self.attach(pword_label2, LABEL_COLUMN, row, 1, 1) 181 | self.attach(self.pword_field2, DATA_COLUMN, row, 1, 1) 182 | 183 | row += 1 184 | # And now an administrative user check 185 | self.adminuser = Gtk.CheckButton( 186 | "This user should have administrative capabilities") 187 | self.attach(self.adminuser, DATA_COLUMN, row, 1, 1) 188 | 189 | row += 1 190 | btnbox = Gtk.ButtonBox() 191 | btnbox.set_spacing(5) 192 | # Lastly the action buttons 193 | self.ok = Gtk.Button("Add now") 194 | self.ok.get_style_context().add_class("suggested-action") 195 | ok_image = Gtk.Image() 196 | ok_image.set_from_icon_name("list-add-symbolic", Gtk.IconSize.BUTTON) 197 | self.ok.set_image(ok_image) 198 | self.ok.set_sensitive(False) 199 | self.ok.connect("clicked", self.add_user) 200 | 201 | self.cancel = Gtk.Button("Cancel") 202 | self.cancel.connect("clicked", lambda x: self.owner.show_main()) 203 | cancel_image = Gtk.Image() 204 | cancel_image.set_from_icon_name("window-close-symbolic", 205 | Gtk.IconSize.BUTTON) 206 | self.cancel.set_image(cancel_image) 207 | 208 | btnbox.set_layout(Gtk.ButtonBoxStyle.START) 209 | btnbox.set_margin_top(10) 210 | btnbox.add(self.ok) 211 | btnbox.add(self.cancel) 212 | self.attach(btnbox, DATA_COLUMN, row, 1, 1) 213 | 214 | for label in [uname_label, rname_label, pword_label, pword_label2]: 215 | label.set_halign(Gtk.Align.START) 216 | 217 | def clear_form(self): 218 | """ Clear the form state """ 219 | items = [ 220 | self.uname_field, 221 | self.rname_field, 222 | self.pword_field, 223 | self.pword_field2 224 | ] 225 | for entry in items: 226 | entry.set_text("") 227 | entry.set_icon_from_icon_name( 228 | Gtk.EntryIconPosition.SECONDARY, None) 229 | for check in [self.adminuser]: 230 | check.set_active(False) 231 | self.adminuser.set_sensitive(True) 232 | 233 | def add_user(self, w=None): 234 | user = User(self.uname_field.get_text(), 235 | self.rname_field.get_text(), 236 | self.pword_field.get_text(), 237 | False, 238 | self.adminuser.get_active()) 239 | self.owner.add_new_user(user) 240 | self.owner.show_main() 241 | 242 | 243 | class InstallerUsersPage(BasePage): 244 | """ User management. """ 245 | 246 | info = None 247 | had_init = False 248 | 249 | def __init__(self): 250 | BasePage.__init__(self) 251 | 252 | self.listbox = Gtk.ListBox() 253 | self.listbox.connect("row-activated", self.activated) 254 | scroller = Gtk.ScrolledWindow(None, None) 255 | scroller.add(self.listbox) 256 | scroller.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) 257 | scroller.set_shadow_type(Gtk.ShadowType.ETCHED_IN) 258 | scroller.get_style_context().set_junction_sides( 259 | Gtk.JunctionSides.BOTTOM) 260 | 261 | # Placeholder stuff 262 | placeholder = Gtk.Label("{}".format( 263 | "You haven\'t added any users yet.")) 264 | placeholder.set_use_markup(True) 265 | placeholder.show() 266 | self.listbox.set_placeholder(placeholder) 267 | 268 | toolbar = Gtk.Toolbar() 269 | toolbar.set_icon_size(Gtk.IconSize.SMALL_TOOLBAR) 270 | toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR) 271 | junctions = Gtk.JunctionSides.TOP 272 | toolbar.get_style_context().set_junction_sides(junctions) 273 | 274 | add = Gtk.ToolButton() 275 | add.connect("clicked", self.add_user) 276 | add.set_icon_name("list-add-symbolic") 277 | toolbar.add(add) 278 | 279 | self.remove = Gtk.ToolButton() 280 | self.remove.set_icon_name("list-remove-symbolic") 281 | self.remove.set_sensitive(False) 282 | self.remove.connect("clicked", self.delete_user) 283 | toolbar.add(self.remove) 284 | 285 | # We use a stack here too, because dialogs are horrible. 286 | self.stack = Gtk.Stack() 287 | self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_UP_DOWN) 288 | self.pack_start(self.stack, True, True, 0) 289 | 290 | main_page = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) 291 | main_page.pack_start(scroller, True, True, 0) 292 | main_page.pack_start(toolbar, False, False, 0) 293 | main_page.set_border_width(40) 294 | 295 | self.stack.add_named(main_page, "main") 296 | 297 | self.add_user_page = NewUserPage(self) 298 | self.stack.add_named(self.add_user_page, "add-user") 299 | 300 | def activated(self, box, row): 301 | if row is None: 302 | self.remove.set_sensitive(False) 303 | return 304 | self.remove.set_sensitive(True) 305 | 306 | def delete_user(self, w=None): 307 | row = self.listbox.get_selected_row() 308 | if row is None: 309 | self.remove.set_sensitive(False) 310 | return 311 | user = row.get_children()[0].user 312 | self.info.users.remove(user) 313 | self.listbox.remove(row) 314 | self.remove.set_sensitive(False) 315 | 316 | if len(self.info.users) == 0: 317 | self.info.owner.set_can_next(False) 318 | 319 | def add_user(self, widget): 320 | admins = [user for user in self.info.users if user.admin] 321 | self.stack.set_visible_child_name("add-user") 322 | if len(admins) == 0: 323 | # Force this new user to be an administrator 324 | self.add_user_page.adminuser.set_sensitive(False) 325 | self.add_user_page.adminuser.set_active(True) 326 | self.info.owner.set_can_previous(False) 327 | 328 | def add_new_user(self, user): 329 | self.info.users.append(user) 330 | user_panel = UserPanel(user) 331 | self.listbox.add(user_panel) 332 | self.listbox.show_all() 333 | self.info.owner.set_can_next(True) 334 | 335 | def show_main(self): 336 | self.stack.set_visible_child_name("main") 337 | self.info.owner.set_can_previous(True) 338 | self.add_user_page.clear_form() 339 | 340 | def prepare(self, info): 341 | self.info = info 342 | if not self.info.users: 343 | self.info.users = list() 344 | 345 | self.info.owner.set_can_previous(True) 346 | self.info.owner.set_can_next(len(self.info.users) > 0) 347 | self.stack.set_visible_child_name("main") 348 | self.show_all() 349 | self.add_user_page.clear_form() 350 | 351 | # Start on the new user page 352 | if not self.had_init: 353 | self.add_user(None) 354 | self.had_init = True 355 | 356 | def get_title(self): 357 | return "Who will use this device?" 358 | 359 | def get_sidebar_title(self): 360 | return "Users" 361 | 362 | def get_name(self): 363 | return "users" 364 | 365 | def get_icon_name(self, plasma=False): 366 | return "system-users" 367 | -------------------------------------------------------------------------------- /os_installer2/permissions.py: -------------------------------------------------------------------------------- 1 | #!/bin/true 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This file is part of os-installer 5 | # 6 | # Copyright 2013-2020 Solus 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | 14 | import os 15 | import pwd 16 | 17 | 18 | class PermissionsManager: 19 | 20 | down_uid = None 21 | down_gid = None 22 | home_dir = None 23 | 24 | def __init__(self): 25 | if "PKEXEC_UID" in os.environ: 26 | id_ = os.environ["PKEXEC_UID"] 27 | try: 28 | uid = int(id_) 29 | self.down_uid = uid 30 | self.down_gid = uid 31 | self.set_details() 32 | except Exception as e: 33 | print("Defaulting on fallback UID: {}".format(e)) 34 | return 35 | if "SUDO_UID" in os.environ: 36 | id_ = os.environ["SUDO_UID"] 37 | try: 38 | uid = int(id_) 39 | self.down_uid = uid 40 | self.down_gid = uid 41 | self.set_details() 42 | except Exception as e: 43 | print("Defaulting on fallback UID: {}".format(e)) 44 | 45 | def set_details(self): 46 | pw = pwd.getpwuid(self.down_uid) 47 | if not pw: 48 | self.home_dir = "/home/live" 49 | return 50 | self.home_dir = pw.pw_dir 51 | 52 | def down_permissions(self): 53 | """ Drop our current permissions """ 54 | try: 55 | os.setresgid(self.down_gid, self.down_gid, 0) 56 | os.setresuid(self.down_uid, self.down_uid, 0) 57 | os.environ['HOME'] = self.home_dir 58 | except Exception as e: 59 | print("Failed to drop permissions: {}".format(e)) 60 | return False 61 | return True 62 | 63 | def up_permissions(self): 64 | """ Elevate our current permissions """ 65 | try: 66 | os.setresuid(0, 0, 0) 67 | os.setresgid(0, 0, 0) 68 | os.environ['HOME'] = '/root' 69 | except Exception as e: 70 | print("Failed to raise permissions: {}".format(e)) 71 | return False 72 | return True 73 | -------------------------------------------------------------------------------- /os_installer2/postinstall.py: -------------------------------------------------------------------------------- 1 | #!/bin/true 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This file is part of os-installer 5 | # 6 | # Copyright 2013-2020 Solus 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | 14 | import subprocess 15 | import os 16 | from collections import OrderedDict 17 | from .diskman import DiskManager 18 | from .diskops import DiskOpCreateSwap, DiskOpUseSwap, DiskOpUseHome 19 | from .diskops import DiskOpCreateBoot 20 | from .diskops import DiskOpCreateLUKSContainer 21 | from .strategy import EmptyDiskStrategy 22 | import shutil 23 | 24 | 25 | def get_part_uuid(path, part_uuid=False): 26 | """ Get the UUID of a given partition """ 27 | col = "PARTUUID" if part_uuid else "UUID" 28 | cmd = "blkid -s {} -o value {}".format(col, path) 29 | try: 30 | o = subprocess.check_output(cmd, shell=True) 31 | o = o.split("\n")[0] 32 | o = o.replace("\r", "").replace("\n", "").strip() 33 | return o 34 | except Exception as e: 35 | print("UUID lookup failed: {}".format(e)) 36 | return None 37 | 38 | 39 | class PostInstallStep: 40 | """ Basic post-install API """ 41 | 42 | # Tracking operations 43 | info = None 44 | 45 | # Installer reference for doing the hard lifting 46 | installer = None 47 | 48 | # Errors for this obj 49 | errors = None 50 | 51 | def __init__(self, info, installer): 52 | self.info = info 53 | self.installer = installer 54 | 55 | def apply(self): 56 | """ Apply this post-install step """ 57 | print("NOT IMPLEMENTED!") 58 | return False 59 | 60 | def get_display_string(self): 61 | return "I AM NOT IMPLEMENTED!" 62 | 63 | def set_errors(self, err): 64 | """ Set the errors for this step """ 65 | self.errors = err 66 | 67 | def get_errors(self): 68 | """ Get the errors, if any, for this step """ 69 | return self.errors 70 | 71 | def run_in_chroot(self, command): 72 | """ Helper to enable quick boolean chroot usage """ 73 | full_cmd = "LC_ALL=C chroot \"{}\" /bin/sh -c \"{}\"".format( 74 | self.installer.get_installer_target_filesystem(), 75 | command) 76 | try: 77 | subprocess.check_call(full_cmd, shell=True) 78 | except Exception as e: 79 | self.set_errors(e) 80 | return False 81 | return True 82 | 83 | def is_long_step(self): 84 | """ Override when this is a long operation and the progressbar should 85 | pulse, so the user doesn't believe the UI locked up """ 86 | return False 87 | 88 | 89 | class PostInstallVfs(PostInstallStep): 90 | """ Set up the virtual filesystems required for all other steps """ 91 | 92 | vfs_points = None 93 | 94 | def __init__(self, info, installer): 95 | PostInstallStep.__init__(self, info, installer) 96 | self.vfs_points = OrderedDict([ 97 | ("/dev", "{}/dev"), 98 | ("/dev/shm", "{}/dev/shm"), 99 | ("/dev/pts", "{}/dev/pts"), 100 | ("/sys", "{}/sys"), 101 | ("/proc", "{}/proc"), 102 | ]) 103 | 104 | def get_display_string(self): 105 | return "Setting up virtual filesystems" 106 | 107 | def apply(self): 108 | target = self.installer.get_installer_target_filesystem() 109 | for source_point in self.vfs_points: 110 | target_point = self.vfs_points[source_point].format(target) 111 | cmd = "mount --bind {} \"{}\"".format(source_point, target_point) 112 | try: 113 | subprocess.check_call(cmd, shell=True) 114 | self.installer.mount_tracker[source_point] = target_point 115 | except Exception as e: 116 | self.set_errors("Failed to bind-mount vfs: {}".format(e)) 117 | return False 118 | return True 119 | 120 | 121 | class PostInstallRemoveLiveConfig(PostInstallStep): 122 | """ Remove the live user from the filesystem """ 123 | 124 | """ Packages that are of no use to the user, i.e. us. """ 125 | live_packages = None 126 | original_source = None 127 | modified_files = None 128 | 129 | def __init__(self, info, installer): 130 | PostInstallStep.__init__(self, info, installer) 131 | self.live_packages = [ 132 | "network-manager-livecd", 133 | "os-installer", 134 | ] 135 | # Find the branding package to nuke 136 | if os.path.exists("/usr/bin/mate-panel"): 137 | self.live_packages.append("mate-desktop-branding-livecd") 138 | elif os.path.exists("/usr/bin/gnome-shell"): 139 | self.live_packages.append("gnome-desktop-branding-livecd") 140 | elif os.path.exists("/usr/bin/plasmashell"): 141 | self.live_packages.append("plasma-desktop-branding-livecd") 142 | else: 143 | self.live_packages.append("budgie-desktop-branding-livecd") 144 | 145 | self.original_source = "/usr/share/os-installer" 146 | self.modified_files = [ 147 | "/etc/gdm/custom.conf" 148 | ] 149 | 150 | def get_display_string(self): 151 | return "Removing live configuration" 152 | 153 | def apply(self): 154 | # Forcibly remove the user (TODO: Make all this configurable... ) 155 | if not self.run_in_chroot("userdel -fr live"): 156 | return False 157 | 158 | packages = [] 159 | packages.extend(self.live_packages) 160 | 161 | # Don't keep GRUB around on UEFI installs, causes confusion. 162 | if self.info.strategy.is_uefi(): 163 | packages.extend(["grub2", "os-prober"]) 164 | 165 | # Return live-specific packages 166 | cmd_remove = "eopkg rmf {} -y".format( 167 | " ".join(packages)) 168 | if not self.run_in_chroot(cmd_remove): 169 | self.set_errors("Failed to remove live packages") 170 | return False 171 | 172 | # Replace the modified 173 | target_fs = self.installer.get_installer_target_filesystem() 174 | 175 | for replacement in self.modified_files: 176 | bname = os.path.basename(replacement) 177 | source_file = os.path.join(self.original_source, bname) 178 | target_file = os.path.join(target_fs, replacement[1:]) 179 | 180 | if not os.path.exists(source_file): 181 | continue 182 | target_dir = os.path.dirname(target_file) 183 | if not os.path.exists(target_dir): 184 | try: 185 | os.makedirs(target_dir, 0o0755) 186 | except Exception as e: 187 | self.set_errors("Cannot mkdir: {}".format(e)) 188 | return False 189 | try: 190 | shutil.copy2(source_file, target_file) 191 | except Exception as e: 192 | self.set_errors("Cannot update file: {}".format(e)) 193 | return False 194 | 195 | # Remove sudo 196 | if not self.run_in_chroot("rm /etc/sudoers.d/os-installer"): 197 | return False 198 | 199 | # Remove history 200 | history_dir = os.path.join(target_fs, "var/lib/eopkg/history") 201 | try: 202 | shutil.rmtree(history_dir) 203 | except Exception as e: 204 | self.set_errors("Cannot remove history: {}".format(e)) 205 | return False 206 | 207 | # Recreate blank directory 208 | try: 209 | os.makedirs(history_dir, 0o0755) 210 | except Exception as e: 211 | self.set_errors("Cannot mkdir: {}".format(e)) 212 | return False 213 | return True 214 | 215 | def is_long_step(self): 216 | """ Have to remove packages and users, compile schemas, etc """ 217 | return True 218 | 219 | 220 | class PostInstallSyncFilesystems(PostInstallStep): 221 | """ Just call sync, nothing fancy """ 222 | 223 | def __init__(self, info, installer): 224 | PostInstallStep.__init__(self, info, installer) 225 | 226 | def get_display_string(self): 227 | return "Flushing buffers to disk.. please wait" 228 | 229 | def is_long_step(self): 230 | return True 231 | 232 | def apply(self): 233 | try: 234 | subprocess.check_call("sync", shell=True) 235 | except: 236 | pass 237 | return True 238 | 239 | 240 | class PostInstallMachineID(PostInstallStep): 241 | """ Initialise the machine-id """ 242 | 243 | def __init__(self, info, installer): 244 | PostInstallStep.__init__(self, info, installer) 245 | 246 | def get_display_string(self): 247 | return "Creating machine-id for new installation" 248 | 249 | def apply(self): 250 | fp = os.path.join(self.installer.get_installer_target_filesystem(), 251 | "etc/machine-id") 252 | # Delete existing machine-id 253 | if os.path.exists(fp): 254 | try: 255 | os.remove(fp) 256 | except Exception as e: 257 | self.set_errors(e) 258 | return False 259 | 260 | # Now create a new machine-id 261 | if not self.run_in_chroot("systemd-machine-id-setup"): 262 | self.set_errors("Failed to construct machine-id") 263 | return False 264 | return True 265 | 266 | 267 | # We use this guy to set the global layout.. 268 | KEYBOARD_CONFIG_TEMPLATE = """ 269 | # Read and parsed by systemd-localed. It's probably wise not to edit this file 270 | # manually too freely. 271 | Section "InputClass" 272 | Identifier "system-keyboard" 273 | MatchIsKeyboard "on" 274 | Option "XkbModel" "%(XKB_MODEL)s" 275 | Option "XkbLayout" "%(XKB_LAYOUT)s" 276 | EndSection 277 | """ 278 | 279 | 280 | class PostInstallKeyboard(PostInstallStep): 281 | """ Set the keyboard layout on the target device """ 282 | 283 | def __init__(self, info, installer): 284 | PostInstallStep.__init__(self, info, installer) 285 | 286 | def get_display_string(self): 287 | return "Storing keyboard configuration" 288 | 289 | def apply(self): 290 | xkb_model = "pc104" 291 | x11dir = os.path.join(self.installer.get_installer_target_filesystem(), 292 | "etc/X11/xorg.conf.d") 293 | x11file = os.path.join(x11dir, "00-keyboard.conf") 294 | 295 | # create the x11 dir 296 | if not os.path.exists(x11dir): 297 | try: 298 | os.makedirs(x11dir, 0o0755) 299 | except Exception as ex: 300 | self.set_errors(ex) 301 | return False 302 | 303 | # set up the template 304 | tmpl = KEYBOARD_CONFIG_TEMPLATE % { 305 | 'XKB_MODEL': xkb_model, 'XKB_LAYOUT': self.info.keyboard 306 | } 307 | 308 | # write the template to disk 309 | tmpl = tmpl.strip() + "\n" 310 | try: 311 | with open(x11file, "w") as xfile: 312 | os.chmod(x11file, 0o0644) 313 | xfile.write(tmpl) 314 | except Exception as ex: 315 | self.set_errors(ex) 316 | return False 317 | return True 318 | 319 | 320 | class PostInstallLocale(PostInstallStep): 321 | """ Set the system locale """ 322 | 323 | def __init__(self, info, installer): 324 | PostInstallStep.__init__(self, info, installer) 325 | 326 | def get_display_string(self): 327 | return "Storing system locale" 328 | 329 | def apply(self): 330 | lang = self.info.locale 331 | 332 | # Dump to locale.conf 333 | fpath = os.path.join(self.installer.get_installer_target_filesystem(), 334 | "etc/locale.conf") 335 | try: 336 | with open(fpath, "w") as localef: 337 | os.chmod(fpath, 0o0644) 338 | if not lang.endswith(".utf8"): 339 | lc = lang.split(".")[0] 340 | lang = "{}.utf8".format(lc) 341 | lang = lang.replace(".utf8", ".UTF-8") 342 | localef.write("LANG={}\n".format(lang)) 343 | except Exception as e: 344 | self.set_errors(e) 345 | return False 346 | return True 347 | 348 | 349 | ADJTIME_LOCAL = """ 350 | 0.0 0 0.0 351 | 0 352 | LOCAL 353 | """ 354 | 355 | 356 | class PostInstallTimezone(PostInstallStep): 357 | """ Set up the timezone """ 358 | 359 | def __init__(self, info, installer): 360 | PostInstallStep.__init__(self, info, installer) 361 | 362 | def get_display_string(self): 363 | return "Storing system timezone" 364 | 365 | def apply(self): 366 | """ Link /etc/localtime up to zoneinfo """ 367 | loc = self.info.timezone 368 | self.run_in_chroot("rm -f /etc/localtime") 369 | cmd = "ln -sf \"/usr/share/zoneinfo/{}\" /etc/localtime".format(loc) 370 | if not self.run_in_chroot(cmd): 371 | self.set_errors("Failed to set timezone") 372 | return False 373 | 374 | if not self.info.windows_present: 375 | return True 376 | 377 | # Set adjtime to local for Windows users 378 | adjp = os.path.join(self.installer.get_installer_target_filesystem(), 379 | "etc/adjtime") 380 | try: 381 | with open(adjp, "w") as adjtime: 382 | os.chmod(adjp, 0o0644) 383 | adjtime.write(ADJTIME_LOCAL.strip() + "\n") 384 | except Exception as e: 385 | print("Warning: Failed to update adjtime: {}".format(e)) 386 | return True 387 | 388 | 389 | class PostInstallUsers(PostInstallStep): 390 | """ Add users to the new installation """ 391 | 392 | normal_groups = None 393 | admin_groups = None 394 | 395 | def __init__(self, info, installer): 396 | PostInstallStep.__init__(self, info, installer) 397 | 398 | self.normal_groups = [ 399 | "audio", 400 | "video", 401 | "cdrom", 402 | "dialout", 403 | "fuse", 404 | "users", 405 | "sambashares", 406 | ] 407 | self.admin_groups = [ 408 | "sudo", 409 | "lpadmin", 410 | "plugdev", 411 | "scanner", 412 | ] 413 | 414 | def get_display_string(self): 415 | return "Creating users" 416 | 417 | def apply(self): 418 | """ Add all of the system users """ 419 | 420 | pwd_file = [] 421 | 422 | # Add all the users. 423 | for user in self.info.users: 424 | groups = [] 425 | groups.extend(self.normal_groups) 426 | if user.admin: 427 | groups.extend(self.admin_groups) 428 | 429 | cmd = "useradd -s {} -c '{}' -G {} -m {}".format( 430 | "/bin/bash", # Default shell in Solus 431 | user.realname, 432 | ",".join(groups), 433 | user.username) 434 | 435 | try: 436 | self.run_in_chroot(cmd) 437 | except Exception as e: 438 | self.set_errors("Cannot configure user: {}".format(e)) 439 | return False 440 | 441 | pwd_file.append("{}:{}".format(user.username, user.password)) 442 | 443 | f = os.path.join(self.installer.get_installer_target_filesystem(), 444 | "tmp/newusers.conf") 445 | 446 | # Pass off the passwords to chpasswd 447 | pwds_done = False 448 | fd = None 449 | try: 450 | fd = open(f, "w") 451 | fd.write("\n".join(pwd_file)) 452 | fd.close() 453 | fd = None 454 | self.run_in_chroot("cat /tmp/newusers.conf | chpasswd") 455 | os.remove(f) 456 | pwds_done = True 457 | except Exception as e: 458 | self.set_errors("Unable to update passwords: {}".format(e)) 459 | 460 | if fd: 461 | fd.close() 462 | 463 | if not pwds_done: 464 | try: 465 | os.remove(f) 466 | except: 467 | pass 468 | return False 469 | 470 | # Disable the root account 471 | if not self.run_in_chroot("passwd -d root"): 472 | self.set_errors("Failed to disable root account") 473 | return False 474 | return True 475 | 476 | 477 | class PostInstallHostname(PostInstallStep): 478 | """ Set up the hostname """ 479 | 480 | def __init__(self, info, installer): 481 | PostInstallStep.__init__(self, info, installer) 482 | 483 | def get_display_string(self): 484 | return "Setting the system hostname" 485 | 486 | def apply(self): 487 | hosts = [ 488 | "127.0.0.1\tlocalhost", 489 | "127.0.0.1\t{}".format(self.info.hostname), 490 | "# The following lines are desirable for IPv6 capable hosts", 491 | "::1 localhost ip6-localhost ip6-loopback", 492 | "fe00::0 ip6-localnet", 493 | "ff00::0 ip6-mcastprefix", 494 | "ff02::1 ip6-allnodes", 495 | "ff02::2 ip6-allrouters", 496 | "ff02::3 ip6-allhosts" 497 | ] 498 | 499 | bpath = self.installer.get_installer_target_filesystem() 500 | hostname_file = os.path.join(bpath, "etc/hostname") 501 | hosts_file = os.path.join(bpath, "etc/hosts") 502 | try: 503 | with open(hostname_file, "w") as hout: 504 | os.chmod(hostname_file, 0o0644) 505 | hout.write("{}\n".format(self.info.hostname)) 506 | with open(hosts_file, "w") as hpout: 507 | os.chmod(hosts_file, 0o0644) 508 | hpout.write("\n".join(hosts) + "\n") 509 | except Exception as e: 510 | self.set_errors("Failed to configure hosts: {}".format(e)) 511 | return False 512 | return True 513 | 514 | 515 | class PostInstallDiskOptimize(PostInstallStep): 516 | """ Optimize disk usage """ 517 | 518 | def __init__(self, info, installer): 519 | PostInstallStep.__init__(self, info, installer) 520 | 521 | def get_display_string(self): 522 | return "Optimizing the disk configuration" 523 | 524 | def apply(self): 525 | dev_path = self.info.strategy.drive.path 526 | 527 | cmd = None 528 | if DiskManager.is_device_ssd(dev_path): 529 | cmd = "systemctl enable fstrim.timer" 530 | else: 531 | # TODO: Support readahead 532 | return True 533 | 534 | if not self.run_in_chroot(cmd): 535 | self.set_errors("Unable to apply disk optimizations") 536 | return False 537 | return True 538 | 539 | class PostInstallUsysconf(PostInstallStep): 540 | """ Run usysconf for the target """ 541 | 542 | def __init__(self, info, installer): 543 | PostInstallStep.__init__(self, info, installer) 544 | 545 | def get_display_string(self): 546 | return "Running usysconf" 547 | 548 | def apply(self): 549 | """ Perform a full usysconf run """ 550 | try: 551 | self.run_in_chroot("usysconf run -f") 552 | except Exception as e: 553 | self.set_errors("Failed to run usysconf: {}".format(e)) 554 | return False 555 | return True 556 | 557 | def is_long_step(self): 558 | """ Its.. just long. Seriously """ 559 | return True 560 | 561 | FSTAB_HEADER = """ 562 | # /etc/fstab: static file system information. 563 | # 564 | # 565 | 566 | # /dev/ROOT / ext3 noatime 0 1 567 | # /dev/SWAP none swap sw 0 0 568 | # /dev/fd0 /mnt/floppy auto noauto 0 0 569 | none /proc proc nosuid,noexec 0 0 570 | none /dev/shm tmpfs defaults 0 0 571 | """ 572 | 573 | 574 | class PostInstallFstab(PostInstallStep): 575 | """ Write the fstab to disk """ 576 | 577 | def __init__(self, info, installer): 578 | PostInstallStep.__init__(self, info, installer) 579 | 580 | def get_display_string(self): 581 | return "Writing filesystem mount points" 582 | 583 | def apply(self): 584 | """ Do the dull task of writing the fstab """ 585 | strat = self.info.strategy 586 | disk = strat.disk 587 | 588 | appends = [] 589 | 590 | ext4_ops = "rw,relatime,errors=remount-ro" 591 | 592 | for op in strat.get_operations(): 593 | # TODO: Add custom mountpoints here! 594 | # Skip swap for GPT/UEFI 595 | if isinstance(op, DiskOpUseHome): 596 | huuid = get_part_uuid(op.home_part.path) 597 | fs = op.home_part_fs 598 | desc = "# {} at time of installation".format(op.home_part.path) 599 | i = "UUID={}\t/home\t{}\t{}\t0\t2" 600 | appends.append(desc) 601 | appends.append(i.format(huuid, fs, ext4_ops)) 602 | continue 603 | elif isinstance(op, DiskOpCreateBoot): 604 | buuid = get_part_uuid(op.part.path) 605 | fs = op.fstype 606 | desc = "# {} at time of installation".format(op.part.path) 607 | i = "UUID={}\t/boot\t{}\t{}\t0\t2" 608 | appends.append(desc) 609 | appends.append(i.format(buuid, fs, ext4_ops)) 610 | continue 611 | 612 | # All swap handling from hereon out 613 | swap_path = None 614 | if isinstance(op, DiskOpCreateSwap): 615 | swap_path = op.part.path 616 | elif isinstance(op, DiskOpUseSwap): 617 | swap_path = op.swap_part.path 618 | 619 | if not swap_path: 620 | continue 621 | 622 | uuid = get_part_uuid(swap_path) 623 | if uuid: 624 | im = "UUID={}\tswap\tswap\tsw\t0\t0".format(uuid) 625 | appends.append(im) 626 | else: 627 | appends.append("{}\tswap\tswap\tsw\t0\t0".format(swap_path)) 628 | 629 | # Add the root partition last 630 | root = strat.get_root_partition() 631 | uuid = get_part_uuid(root) 632 | appends.append("# {} at time of installation".format(root)) 633 | 634 | appends.append("UUID={}\t/\text4\t{}\t0\t1".format(uuid, ext4_ops)) 635 | 636 | fp = os.path.join(self.installer.get_installer_target_filesystem(), 637 | "etc/fstab") 638 | 639 | try: 640 | with open(fp, "w") as fstab: 641 | fstab.write(FSTAB_HEADER.strip() + "\n") 642 | fstab.write("\n".join(appends) + "\n") 643 | except Exception as e: 644 | self.set_errors("Failed to write fstab: {}".format(e)) 645 | return False 646 | return True 647 | 648 | 649 | class PostInstallBootloader(PostInstallStep): 650 | """ Install the bootloader itself """ 651 | 652 | # We record swap uuid into resume= parameter 653 | swap_uuid = None 654 | 655 | def __init__(self, info, installer): 656 | PostInstallStep.__init__(self, info, installer) 657 | 658 | def get_display_string(self): 659 | return "Configuring bootloader.. please wait" 660 | 661 | def apply(self): 662 | # Determine the swap path 663 | swap_path = None 664 | for op in self.info.strategy.get_operations(): 665 | if isinstance(op, DiskOpCreateSwap): 666 | swap_path = op.part.path 667 | elif isinstance(op, DiskOpUseSwap): 668 | swap_path = op.swap_part.path 669 | 670 | if swap_path is not None: 671 | self.swap_uuid = get_part_uuid(swap_path) 672 | 673 | if self.info.strategy.is_uefi(): 674 | return self.apply_boot_loader() 675 | return self.apply_bios() 676 | 677 | def is_long_step(self): 678 | """ UEFI no, GRUB yes. """ 679 | return not self.info.strategy.is_uefi() 680 | 681 | def get_luks_uuid(self): 682 | """ Get the cached LUKS Container UUID """ 683 | luks_uuid = None 684 | for op in self.info.strategy.get_operations(): 685 | if isinstance(op, DiskOpCreateLUKSContainer): 686 | luks_uuid = op.crypto_uuid 687 | break 688 | return luks_uuid 689 | 690 | def is_encrypted_install(self): 691 | strategy = self.info.strategy 692 | if isinstance(strategy, EmptyDiskStrategy): 693 | if not strategy.use_lvm2: 694 | return False 695 | if not strategy.use_encryption: 696 | return False 697 | else: 698 | return False 699 | return True 700 | 701 | def is_lvm2_install(self): 702 | strategy = self.info.strategy 703 | if isinstance(strategy, EmptyDiskStrategy): 704 | if strategy.use_lvm2: 705 | return True 706 | return False 707 | 708 | def apply_boot_loader(self): 709 | """ Invoke clr-boot-manager itself """ 710 | target = self.installer.get_installer_target_filesystem() 711 | 712 | kdir = os.path.join(target, "etc/kernel/cmdline.d") 713 | kresumefile = os.path.join(kdir, "10_resume.conf") 714 | 715 | # Attempt to mount efivarfs dir in order to create the EFI boot entry 716 | # No big deal if it fails, we'll rely on shim's fallback to create it. 717 | efivardir = "/sys/firmware/efi/efivars" 718 | target_point = "{}{}".format(target, efivardir) 719 | efivar_cmd = "mount --types efivarfs {} \"{}\"".format( 720 | efivardir, 721 | target_point) 722 | try: 723 | subprocess.check_call(efivar_cmd, shell=True) 724 | self.installer.mount_tracker[efivardir] = target_point 725 | except Exception as e: 726 | print("Error mounting efivar vfs: {}".format(e)) 727 | print("Buggy UEFI firmware, relying on shim's fallback") 728 | 729 | # Write out the resume= parameter for clr-boot-manager 730 | if self.swap_uuid is not None: 731 | if not os.path.exists(kdir): 732 | try: 733 | os.makedirs(kdir, 00755) 734 | with open(kresumefile, "w") as kfile_output: 735 | swap = "resume=UUID={}".format(self.swap_uuid) 736 | kfile_output.write(swap) 737 | except Exception as ex: 738 | self.set_errors("Error with kernel config: {}".format(ex)) 739 | return False 740 | 741 | cmd = "clr-boot-manager update" 742 | if not self.run_in_chroot(cmd): 743 | self.set_errors("Failed to update bootloader configuration") 744 | return False 745 | return True 746 | 747 | def apply_bios(self): 748 | """ Take the BIOS approach to bootloader configuration """ 749 | if not self.info.bootloader_install: 750 | # Still need detecting from other distros 751 | return self.apply_boot_loader() 752 | cmd = "grub-install --force \"{}\"".format(self.info.bootloader_sz) 753 | if not self.run_in_chroot(cmd): 754 | self.set_errors("Failed to install GRUB bootloader") 755 | return False 756 | # Proxy back to CBM 757 | return self.apply_boot_loader() 758 | 759 | def get_ichild(self, root, child): 760 | t1 = os.path.join(root, child) 761 | if os.path.exists(t1) or not os.path.exists(root): 762 | return t1 763 | try: 764 | for i in os.listdir(root): 765 | i2 = i.lower() 766 | if i2 == child: 767 | return os.path.join(root, i) 768 | except Exception as ex: 769 | print("Error obtaining {} dir: {}".format(child, ex)) 770 | return t1 771 | 772 | def get_efi_dir(self, base): 773 | return self.get_ichild(base, "EFI") 774 | -------------------------------------------------------------------------------- /os_installer2/tz.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; Mode: Python; indent-tabs-mode: nil; tab-width: 4 -*- 2 | 3 | # Copyright (C) 2006, 2007 Canonical Ltd. 4 | # Written by Colin Watson . 5 | # 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 19 | 20 | from __future__ import print_function 21 | 22 | import datetime 23 | import hashlib 24 | import os 25 | import sys 26 | import time 27 | import xml.dom.minidom 28 | 29 | 30 | TZ_DATA_FILE = '/usr/share/zoneinfo/zone.tab' 31 | ISO_3166_FILE = '/usr/share/xml/iso-codes/iso_3166.xml' 32 | 33 | 34 | def _seconds_since_epoch(dt): 35 | # TODO cjwatson 2006-02-23: %s escape is not portable 36 | return int(dt.replace(tzinfo=None).strftime('%s')) 37 | 38 | 39 | class SystemTzInfo(datetime.tzinfo): 40 | def __init__(self, tz=None): 41 | self.tz = tz 42 | 43 | def _select_tz(self): 44 | tzbackup = None 45 | if 'TZ' in os.environ: 46 | tzbackup = os.environ['TZ'] 47 | if self.tz is not None: 48 | os.environ['TZ'] = self.tz 49 | time.tzset() 50 | return tzbackup 51 | 52 | def _restore_tz(self, tzbackup): 53 | if tzbackup is None: 54 | if 'TZ' in os.environ: 55 | del os.environ['TZ'] 56 | else: 57 | os.environ['TZ'] = tzbackup 58 | time.tzset() 59 | 60 | def utcoffset(self, dt): 61 | tzbackup = self._select_tz() 62 | try: 63 | if time.daylight == 0: 64 | # no DST information 65 | dstminutes = -time.timezone / 60 66 | else: 67 | localtime = time.localtime(_seconds_since_epoch(dt)) 68 | if localtime.tm_isdst != 1: 69 | # not in DST 70 | dstminutes = -time.timezone / 60 71 | else: 72 | # in DST 73 | dstminutes = -time.altzone / 60 74 | return datetime.timedelta(minutes=int(dstminutes)) 75 | finally: 76 | self._restore_tz(tzbackup) 77 | 78 | def rawutcoffset(self, unused_dt): 79 | tzbackup = self._select_tz() 80 | try: 81 | dstminutes = -time.timezone / 60 82 | return datetime.timedelta(minutes=int(dstminutes)) 83 | finally: 84 | self._restore_tz(tzbackup) 85 | 86 | def dst(self, dt): 87 | tzbackup = self._select_tz() 88 | try: 89 | if time.daylight == 0: 90 | # no DST information, so assume no DST; None would be more 91 | # accurate but causes awkwardness in fromutc() 92 | return datetime.timedelta(0) 93 | else: 94 | localtime = time.localtime(_seconds_since_epoch(dt)) 95 | if localtime.tm_isdst != 1: 96 | # not in DST 97 | return datetime.timedelta(0) 98 | else: 99 | dstminutes = (time.timezone - time.altzone) / 60 100 | return datetime.timedelta(minutes=int(dstminutes)) 101 | finally: 102 | self._restore_tz(tzbackup) 103 | 104 | def tzname(self, unused_dt): 105 | return self.tz 106 | 107 | def tzname_letters(self, dt): 108 | tzbackup = self._select_tz() 109 | try: 110 | localtime = time.localtime(_seconds_since_epoch(dt)) 111 | return time.strftime('%Z', localtime) 112 | finally: 113 | self._restore_tz(tzbackup) 114 | 115 | 116 | class Iso3166(object): 117 | def __init__(self): 118 | self.names = {} 119 | document = xml.dom.minidom.parse(ISO_3166_FILE) 120 | entries = document.getElementsByTagName('iso_3166_entries')[0] 121 | self.handle_entries(entries) 122 | 123 | def handle_entries(self, entries): 124 | for entry in entries.getElementsByTagName('iso_3166_entry'): 125 | self.handle_entry(entry) 126 | 127 | def handle_entry(self, entry): 128 | if (entry.hasAttribute('alpha_2_code') and 129 | (entry.hasAttribute('common_name') or 130 | entry.hasAttribute('name'))): 131 | alpha_2_code = entry.getAttribute('alpha_2_code') 132 | if entry.hasAttribute('common_name'): 133 | name = entry.getAttribute('common_name').encode('utf-8') 134 | else: 135 | name = entry.getAttribute('name').encode('utf-8') 136 | self.names[alpha_2_code] = name 137 | 138 | 139 | # Much of the Location and Database classes are a rough translation of 140 | # gnome-system-tools/src/time/tz.c. Thanks to Hans Petter Jansson 141 | # for that. 142 | 143 | def _parse_position(position, wholedigits): 144 | if position == '' or len(position) < 4 or wholedigits > 9: 145 | return 0.0 146 | wholestr = position[:wholedigits + 1] 147 | fractionstr = position[wholedigits + 1:] 148 | whole = float(wholestr) 149 | fraction = float(fractionstr) 150 | if whole >= 0.0: 151 | return whole + fraction / pow(10.0, len(fractionstr)) 152 | else: 153 | return whole - fraction / pow(10.0, len(fractionstr)) 154 | 155 | 156 | class Location(object): 157 | def __init__(self, zonetab_line, iso3166): 158 | bits = zonetab_line.rstrip().split('\t', 3) 159 | latlong = bits[1] 160 | latlongsplit = latlong.find('-', 1) 161 | if latlongsplit == -1: 162 | latlongsplit = latlong.find('+', 1) 163 | if latlongsplit != -1: 164 | latitude = latlong[:latlongsplit] 165 | longitude = latlong[latlongsplit:] 166 | else: 167 | latitude = latlong 168 | longitude = '+0' 169 | 170 | self.country = bits[0] 171 | if self.country in iso3166.names: 172 | self.human_country = iso3166.names[self.country] 173 | else: 174 | self.human_country = self.country 175 | self.zone = bits[2] 176 | self.human_zone = self.zone.replace('_', ' ').split('/')[-1] 177 | if len(bits) > 3: 178 | self.comment = bits[3] 179 | else: 180 | self.comment = None 181 | self.latitude = _parse_position(latitude, 2) 182 | self.longitude = _parse_position(longitude, 3) 183 | 184 | # Grab md5sum of the timezone file for later comparison 185 | try: 186 | zone_path = os.path.join('/usr/share/zoneinfo', self.zone) 187 | with open(zone_path, 'rb') as tz_file: 188 | self.md5sum = hashlib.md5(tz_file.read()).digest() 189 | except IOError: 190 | self.md5sum = None 191 | 192 | try: 193 | today = datetime.datetime.today() 194 | except (ValueError, OverflowError): 195 | # Some versions of Python have problems with clocks set before 196 | # the epoch (http://python.org/sf/1646728). Assuming that the 197 | # time is set to the epoch will at least let us avoid crashing, 198 | # although the UTC offset and zone letters may be wrong. 199 | today = datetime.datetime.fromtimestamp(0) 200 | self.info = SystemTzInfo(self.zone) 201 | self.utc_offset = self.info.utcoffset(today) 202 | self.raw_utc_offset = self.info.rawutcoffset(today) 203 | self.zone_letters = self.info.tzname_letters(today) 204 | 205 | 206 | class _Database(object): 207 | def __init__(self): 208 | self.locations = [] 209 | iso3166 = Iso3166() 210 | with open(TZ_DATA_FILE) as tzdata: 211 | for line in tzdata: 212 | if line.startswith('#'): 213 | continue 214 | self.locations.append(Location(line, iso3166)) 215 | 216 | # Build mappings from timezone->location and country->locations 217 | self.cc_to_locs = {} 218 | self.tz_to_loc = {} 219 | for loc in self.locations: 220 | self.tz_to_loc[loc.zone] = loc 221 | if loc.country in self.cc_to_locs: 222 | self.cc_to_locs[loc.country] += [loc] 223 | else: 224 | self.cc_to_locs[loc.country] = [loc] 225 | 226 | def get_loc(self, tz): 227 | # Sometimes we'll encounter timezones that aren't really 228 | # city-zones, like "US/Eastern" or "Mexico/General". So first, 229 | # we check if the timezone is known. If it isn't, we search for 230 | # one with the same md5sum and make a reference to it 231 | try: 232 | return self.tz_to_loc[tz] 233 | except: 234 | try: 235 | zone_path = os.path.join('/usr/share/zoneinfo', tz) 236 | with open(zone_path, 'rb') as tz_file: 237 | md5sum = hashlib.md5(tz_file.read()).digest() 238 | 239 | for loc in self.locations: 240 | if md5sum == loc.md5sum: 241 | self.tz_to_loc[tz] = loc 242 | return loc 243 | except IOError: 244 | pass 245 | 246 | # If not found, oh well, just warn and move on. 247 | print('Could not understand timezone', tz, file=sys.stderr) 248 | self.tz_to_loc[tz] = None # save it for the future 249 | return None 250 | 251 | 252 | _database = None 253 | 254 | 255 | def Database(): 256 | global _database 257 | if not _database: 258 | _database = _Database() 259 | return _database 260 | -------------------------------------------------------------------------------- /os_installer2/users.py: -------------------------------------------------------------------------------- 1 | #!/bin/true 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This file is part of os-installer 5 | # 6 | # Copyright 2013-2020 Solus 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | 14 | 15 | """ Allowed regex for a username """ 16 | USERNAME_REGEX = "^[a-z_][a-z0-9_-]*[$]?$" 17 | 18 | """ Minimum password length """ 19 | PASSWORD_LENGTH = 6 20 | 21 | 22 | class User: 23 | """ Nothing fancifull here, just a user object creation thinger """ 24 | 25 | # System username 26 | username = None 27 | 28 | # Real name (i.e. GECOS) 29 | realname = None 30 | 31 | # Chosen password 32 | password = None 33 | 34 | # Administrator? i.e. sudo 35 | admin = False 36 | 37 | # Autologin? Not yet implemented.. 38 | autologin = False 39 | 40 | def __init__(self, username, realname, password, autologin, admin): 41 | """ Create a new user with the given fields """ 42 | self.username = username 43 | self.realname = realname 44 | self.password = password 45 | self.autologin = autologin 46 | self.admin = admin 47 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name = "os-installer", 5 | version = "16.0", 6 | author = "Solus", 7 | author_email = "copyright@getsol.us", 8 | description = ("Operating System Installer"), 9 | license = "GPL-2.0", 10 | url = "https://github.com/getsolus/os-installer", 11 | packages = ['os_installer2', 'os_installer2.pages'], 12 | scripts = ['os-installer-gtk'], 13 | classifiers = [ "License :: OSI Approved :: GPL-2.0 License"], 14 | package_data = {'os_installer2': ['data/*.png', 'data/*.svg', 'data/*.css']}, 15 | ) 16 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pep8 os-installer-gtk os_installer2/*.py os_installer2/pages/*.py || exit 1 4 | flake8 os-installer-gtk os_installer2/*.py os_installer2/pages/*.py || exit 1 5 | 6 | # check use of %s 7 | t=`grep '%s' os-installer-gtk os_installer2/*.py os_installer2/pages/*.py | grep -v tz.py` 8 | if [[ $t == "" ]]; then 9 | exit 0 10 | fi 11 | echo "Found use of '%s' in tree" 12 | echo "$t" 13 | exit 1 14 | --------------------------------------------------------------------------------