├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── Vagrantfile ├── bin └── dr ├── dr.conf-example ├── dr.gemspec ├── lib ├── dr.rb └── dr │ ├── build_environments.rb │ ├── buildroot.rb │ ├── config.rb │ ├── debpackage.rb │ ├── gitpackage.rb │ ├── gnupg.rb │ ├── logger.rb │ ├── package.rb │ ├── pkgversion.rb │ ├── repo.rb │ ├── server.rb │ ├── shellcmd.rb │ ├── threadpool.rb │ ├── utils.rb │ └── version.rb └── spec └── pkgversion_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | *.swp 19 | .DS_store 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | before_install: 3 | - gem install bundler -v 1.11.2 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in mkpkg.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kano Repository Manager 2 | 3 | [![Gem Version](https://badge.fury.io/rb/dr.svg)](http://badge.fury.io/rb/dr) [![Build Status](https://travis-ci.org/KanoComputing/kano-repository-manager.svg)](https://travis-ci.org/KanoComputing/kano-repository-manager) [![Join the chat at https://gitter.im/KanoComputing/kano-repository-manager](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/KanoComputing/kano-repository-manager?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | **dr** (stands for debian repository) is a Debian repository management tool. 6 | It will help you set up and maintain your own small package repository for any 7 | Debian-based distribution. You can keep your sources in **git** and use the 8 | **dr** tool to manage builds, versions, and releases. It works particularly 9 | well in case your development is very fast and you ship new versions of 10 | your packages often (even several times a day). 11 | 12 | The following diagram illustrates how `dr` works. It takes source packages 13 | that are managed in git repositories, builds them and serves them in 14 | different suites. For more information, please see this 15 | [project's wiki](https://github.com/KanoComputing/kano-package-system/wiki). 16 | 17 |

18 | How dr operates 20 |

21 | 22 | It is the tool we use to manage our software repository and the custom 23 | packages for **Kano OS**. The application is written in **Ruby**, building 24 | on top of many other tools (such as reprepro, debuild, debhelper, and others). 25 | 26 | Here is like it looks like in the terminal: 27 | 28 | ![Example of using dr](http://linuxwell.com/assets/images/posts/tco-example.png) 29 | 30 | 31 | ## Requirements 32 | 33 | In order to generate new build roots you will require `debootstrap`. A bug in 34 | versions prior to 1.0.72 would cause issues with unpacking packages in a 35 | foreign environment before processing Pre-Depends, causing packages to fail to 36 | install. To avoid this issue, ensure that you are using `debootstrap >= 1.0.72`. 37 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new 5 | 6 | task :default => :spec 7 | task :test => :spec 8 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "debian/jessie64" 6 | 7 | config.vm.network "forwarded_port", guest: 80, host: 8080 8 | config.vm.network "private_network", ip: "192.168.33.10" 9 | config.vm.network "public_network" 10 | 11 | config.vm.provision "shell", inline: <<-SHELL 12 | apt-get update 13 | apt-get install -y \ 14 | git \ 15 | tar \ 16 | gzip \ 17 | devscripts \ 18 | debhelper \ 19 | debootstrap \ 20 | qemu-user-static \ 21 | ruby \ 22 | rubygems \ 23 | build-essential \ 24 | curl \ 25 | reprepro \ 26 | rng-tools \ 27 | dpkg-sig \ 28 | ruby-dev \ 29 | vim 30 | 31 | cd /vagrant && gem build dr.gemspec && gem install dr 32 | 33 | SHELL 34 | end 35 | -------------------------------------------------------------------------------- /bin/dr: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Copyright (C) 2014 Kano Computing Ltd. 4 | # License: http://www.gnu.org/licenses/gpl-2.0.txt GNU General Public License v2 5 | 6 | require "thor" 7 | require "fileutils" 8 | require "io/console" 9 | 10 | require "dr" 11 | require "dr/repo" 12 | require "dr/gitpackage" 13 | require "dr/debpackage" 14 | require "dr/buildroot" 15 | require "dr/pkgversion" 16 | 17 | require "dr/shellcmd" 18 | require "dr/logger" 19 | require "dr/config" 20 | require "dr/server" 21 | require "dr/threadpool" 22 | 23 | 24 | class ExtendedThor < Thor 25 | private 26 | include Dr::Logger 27 | 28 | 29 | def initialize(*args) 30 | super 31 | Dr::Logger::set_verbosity options[:verbosity] 32 | if not options[:log_file].nil? 33 | file = File.open options[:log_file],"w" 34 | Dr::Logger::set_logfile(file) 35 | end 36 | end 37 | 38 | 39 | def get_repo_handle 40 | if options.has_key? "repo" 41 | if Dr.config.repositories.has_key? options["repo"] 42 | Dr::Repo.new Dr.config.repositories[options["repo"]][:location] 43 | else 44 | Dr::Repo.new options["repo"] 45 | end 46 | else 47 | if Dr.config.default_repo != nil 48 | Dr::Repo.new Dr.config.repositories[Dr.config.default_repo][:location] 49 | else 50 | log :warn, "No repo was specified, using '#{Dir.pwd}'." 51 | Dr::Repo.new Dir.pwd 52 | end 53 | end 54 | end 55 | end 56 | 57 | 58 | class Archive < ExtendedThor 59 | #desc "save TAG", "make a snapshot of the current archive" 60 | #def save(tag) 61 | #end 62 | 63 | #desc "restore TAG", "replace the current archive with an earlier snapshot" 64 | #def restore(tag) 65 | #end 66 | 67 | #desc "list-versions", "show all snapshots" 68 | #map "list-versions" => :list_versions 69 | #def list_versions 70 | #end 71 | end 72 | 73 | class Conf < ExtendedThor 74 | desc "repo KEY [VALUE]", "Configuration of the whole repository" 75 | def repo(key, value=nil) 76 | repo = get_repo_handle 77 | 78 | metadata = repo.get_configuration 79 | 80 | if value == nil 81 | value = dot_get_value metadata, key 82 | puts value if value 83 | else 84 | repo.set_configuration dot_set_value metadata, key, value 85 | end 86 | end 87 | 88 | desc "package PKG-NAME KEY [VALUE]", "Package-specific configuration options" 89 | def package(pkg_name, key, value=nil) 90 | repo = get_repo_handle 91 | pkg = repo.get_package pkg_name 92 | 93 | metadata = pkg.get_configuration 94 | 95 | if value == nil 96 | value = dot_get_value metadata, key 97 | puts value if value 98 | else 99 | pkg.set_configuration dot_set_value metadata, key, value 100 | end 101 | end 102 | 103 | private 104 | def dot_set_value(dict, key, value) 105 | levels = key.split(".").map {|l| l.to_sym} 106 | raise "Incorrect key" if levels.length == 0 107 | 108 | begin 109 | last = levels.pop 110 | object = dict 111 | levels.each do |l| 112 | object[l] = {} unless object.has_key? l 113 | object = object[l] 114 | end 115 | 116 | if value.length > 0 117 | object[last] = value 118 | else 119 | object.delete last 120 | end 121 | rescue 122 | log :err, "The configuration key '#{key}' isn't right" 123 | raise "Incorrect key" 124 | end 125 | 126 | dict 127 | end 128 | 129 | def dot_get_value(dict, key) 130 | levels = key.split(".").map {|l| l.to_sym} 131 | raise "Incorrect key" if levels.length == 0 132 | 133 | begin 134 | last = levels.pop 135 | object = dict 136 | levels.each do |l| 137 | object = object[l] 138 | end 139 | return object[last] 140 | rescue 141 | log :err, "The configuration key '#{key}' isn't right" 142 | raise "Incorrect key" 143 | end 144 | end 145 | end 146 | 147 | class List < ExtendedThor 148 | desc "packages", "Show a list of source packages in the repo" 149 | def packages() 150 | repo = get_repo_handle 151 | log :info, "Listing all source packages in the repository" 152 | 153 | repo.list_packages.each do |pkg| 154 | log :info, " #{pkg.name.fg "orange"}" 155 | end 156 | end 157 | 158 | desc "gitrepos SUITE", "Show a list of git repo names for source packages" 159 | def gitrepos(suite) 160 | repo = get_repo_handle 161 | 162 | suites = repo.get_suites 163 | exists = suites.inject(false) { |r, s| r || s.include?(suite) } 164 | raise "Suite '#{suite}' doesn't exist" unless exists 165 | 166 | log :info, "Listing all git repos used by #{suite.fg "blue"}" 167 | 168 | suite = repo.codename_to_suite suite 169 | suite_packages = repo.list_packages 170 | suite_packages.each do |pkg| 171 | if pkg.is_a? Dr::GitPackage 172 | versions = repo.get_subpackage_versions pkg.name 173 | reponame = pkg.get_repo_name 174 | if not versions[suite].empty? 175 | log :info, " #{reponame.fg "orange"}" 176 | end 177 | end 178 | end 179 | end 180 | 181 | 182 | desc "versions PKG-NAME", "DEPRECATED, please use builds instead" 183 | def versions(pkg_name) 184 | log :warn, "This subcommand is deprecated, please use builds instead" 185 | builds pkg_name 186 | end 187 | 188 | desc "builds PKG-NAME", "Show the history of all builds of a package" 189 | def builds(pkg_name) 190 | repo = get_repo_handle 191 | log :info, "Listing all builds of #{pkg_name.style "pkg-name"}" 192 | 193 | suites = repo.get_suites 194 | 195 | pkg = repo.get_package pkg_name 196 | pkg.history.each do |version| 197 | line = "#{version.style "version"}" 198 | 199 | if pkg.build_exists? version 200 | debs = repo.get_build pkg.name, version 201 | 202 | metadata = repo.get_build_metadata pkg.name, version 203 | if metadata.has_key? "branch" 204 | open = "{".fg "dark-grey" 205 | close = "}".fg "dark-grey" 206 | line << " " + open + metadata["branch"].fg("blue") + close 207 | end 208 | 209 | subpkgs = debs.map { |p| File.basename(p).split("_")[0] } 210 | end 211 | 212 | open = "[".fg "dark-grey" 213 | close = "]".fg "dark-grey" 214 | 215 | if subpkgs.length == 0 216 | line << " " + open + "broken".fg("red") + close 217 | else 218 | suites.each do |suite, codename| 219 | codename = suite if codename == nil 220 | colour = suite_to_colour suite 221 | 222 | all_included = true 223 | subpkgs.each do |subpkg| 224 | unless repo.query_for_deb_version(suite, subpkg) == version 225 | all_included = false 226 | end 227 | end 228 | 229 | if all_included 230 | if colour 231 | line << " " + open + codename.fg(colour) + close 232 | else 233 | line << " " + open + codename + close 234 | end 235 | end 236 | end 237 | end 238 | log :info, " #{line}" 239 | end 240 | end 241 | 242 | 243 | desc "suite SUITE", "Show the names and versions of packages in the suite" 244 | def suite(suite) 245 | repo = get_repo_handle 246 | 247 | suites = repo.get_suites 248 | exists = suites.inject(false) { |r, s| r || s.include?(suite) } 249 | raise "Suite '#{suite}' doesn't exist" unless exists 250 | 251 | log :info, "Listing all the packages in #{suite.fg "blue"}" 252 | 253 | suite = repo.codename_to_suite suite 254 | suite_packages = repo.list_packages 255 | suite_packages.each do |pkg| 256 | versions = repo.get_subpackage_versions pkg.name 257 | unless versions[suite].empty? 258 | if versions[suite].length == 1 && versions[suite].has_key?(pkg.name) 259 | log :info, " #{pkg.name.style "pkg-name"} " + 260 | "#{versions[suite][pkg.name].style "version"}" 261 | else 262 | log :info, " #{pkg.name.style "pkg-name"}" 263 | versions[suite].each do |subpkg, version| 264 | log :info, " #{subpkg.style "subpkg-name"} " + 265 | "#{version.style "version"}" 266 | end 267 | end 268 | end 269 | end 270 | end 271 | 272 | desc "codenames", "Show the codenames of the configured suites" 273 | def codenames 274 | repo = get_repo_handle 275 | 276 | suites = repo.get_suites 277 | 278 | suites.each do |suite, codename| 279 | codename = suite if codename == nil 280 | colour = suite_to_colour suite 281 | 282 | log :info, "#{codename.fg colour}: #{suite}" 283 | end 284 | end 285 | 286 | private 287 | def suite_to_colour(suite) 288 | colour = case suite 289 | when "stable-security" then "magenta" 290 | when "stable" then "red" 291 | when "testing" then "yellow" 292 | when "unstable" then "green" 293 | else "cyan" end 294 | return colour 295 | end 296 | end 297 | 298 | 299 | class RepoCLI < ExtendedThor 300 | class_option :repo, :type => :string, :aliases => "-r" 301 | class_option :verbosity, :type => :string, :aliases => "-v", :default => "verbose" 302 | class_option :log_file, :type => :string, :aliases => "-l", :default => nil 303 | 304 | 305 | desc "init [LOCATION]", "setup a whole new repository from scratch" 306 | def init(location=".") 307 | log :info, "Initialising a debian repository at '#{location.fg("blue")}'" 308 | 309 | repo_conf = { 310 | :name => "Debian Repository", 311 | :desc => "", 312 | :arches => ["amd64"], 313 | :components => ["main"], 314 | :suites => ["stable-security", "stable", "testing", "unstable"], 315 | :build_environment => :kano, 316 | :codenames => [] 317 | } 318 | 319 | name = ask " Repository name "<< "[#{repo_conf[:name].fg("yellow")}]:" 320 | repo_conf[:name] = name if name.length > 0 321 | 322 | desc = ask " Description [#{repo_conf[:desc]}]:" 323 | repo_conf[:desc] = desc if desc.length > 0 324 | 325 | puts " Default build environment [pick one]: " 326 | Dr::config.build_environments.each do |id, benv| 327 | puts " [#{id.to_s.fg "blue"}] #{benv[:name]}" 328 | end 329 | 330 | benv = nil 331 | loop do 332 | benv_str = ask " Your choice [#{repo_conf[:build_environment].to_s.fg "yellow"}]:" 333 | benv = benv_str.to_sym 334 | break if Dr::config.build_environments.has_key? benv_str.to_sym 335 | end 336 | repo_conf[:build_environment] = benv 337 | 338 | # guess repo arches 339 | repo_conf[:arches] = Dr::config.build_environments[benv][:arches] 340 | 341 | loop do 342 | str = ask " Architectures [#{repo_conf[:arches].join(" ").fg("yellow")}]:" 343 | break if str.length == 0 344 | 345 | # Determine the available architectures 346 | avail = Dr.config.build_environments[benv][:arches] 347 | 348 | arches = str.split(/\s+/) 349 | arches_valid = arches.reduce(true) do |acc, arch| 350 | if !avail.include?(arch) 351 | puts " " + "#{arch.fg "yellow"}" + 352 | " not supported by the build environments you selected" 353 | acc = false 354 | end 355 | 356 | acc 357 | end 358 | next if !arches_valid 359 | 360 | repo_conf[:arches] = arches 361 | break 362 | end 363 | 364 | components = ask " Components [#{repo_conf[:components].join(" ").fg("yellow")}]:" 365 | repo_conf[:components] = components.split(/\s+/) if components.length > 0 366 | 367 | repo_conf[:gpg_name] = "" 368 | while repo_conf[:gpg_name].length == 0 369 | repo_conf[:gpg_name] = ask " Cert owner name (#{"required".fg("red")}):" 370 | repo_conf[:gpg_name].strip! 371 | end 372 | 373 | repo_conf[:gpg_mail] = "" 374 | while repo_conf[:gpg_mail].length == 0 375 | repo_conf[:gpg_mail] = ask " Cert owner e-mail (#{"required".fg("red")}):" 376 | repo_conf[:gpg_mail].strip! 377 | end 378 | 379 | print " Passphrase (#{"optional".fg("green")}): " 380 | repo_conf[:gpg_pass] = STDIN.noecho(&:gets).chomp 381 | print "\n" 382 | 383 | repo_conf[:suites].each do |s| 384 | codename = ask " Codename for '#{s.fg("yellow")}':" 385 | repo_conf[:codenames].push codename 386 | end 387 | 388 | # TODO: Add CLI command to add suites 389 | extra_suite_count = ask " Additional suites [#{'0'.fg("yellow")}]:" 390 | extra_suite_count.to_i.times do |idx| 391 | suite_name = nil 392 | loop do 393 | suite_name = ask " Suite #{idx + 1} name (#{"required".fg("red")}):" 394 | if repo_conf[:suites].include? suite_name 395 | log :warn, "Suite already exists, try again".fg("red") 396 | next 397 | end 398 | break unless suite_name.empty? 399 | end 400 | repo_conf[:suites].push suite_name 401 | 402 | codename = nil 403 | loop do 404 | codename = ask " Codename for suite #{idx + 1} (#{"required".fg("red")}):" 405 | if repo_conf[:codenames].include? codename 406 | log :warn, "Codename already exists, try again".fg("red") 407 | next 408 | end 409 | break unless codename.empty? 410 | end 411 | repo_conf[:codenames].push codename 412 | end 413 | 414 | r = Dr::Repo.new location 415 | r.setup repo_conf 416 | end 417 | 418 | 419 | desc "add", "introduce a new package to the build system" 420 | method_option :git, :aliases => "-g", 421 | :desc => "Add source package managed in a git repo" 422 | method_option :deb, :aliases => "-d", 423 | :desc => "Add a prebuilt binary deb package only" 424 | method_option :force, :aliases => "-f", :type => :boolean, 425 | :desc => "Proceed even if the package already exists" 426 | method_option :branch, :aliases => "-b", 427 | :desc => "Set a default branch other than master (valid only with --git)" 428 | def add 429 | repo = get_repo_handle 430 | 431 | case 432 | when options.has_key?("git") 433 | branch = "master" 434 | branch = options["branch"] if options.has_key? "branch" 435 | 436 | Dr::GitPackage::setup repo, options["git"], branch 437 | when options.has_key?("deb") 438 | Dr::DebPackage::setup repo, options["deb"], options["force"] 439 | else 440 | raise ArgumentError, "Either --git or --deb must be specified" 441 | end 442 | end 443 | 444 | 445 | desc "build PKG-NAME", "build a package from the sources" 446 | method_option :branch, :aliases => "-b", :type => :string, 447 | :desc => "build from a different branch" 448 | method_option :push, :aliases => "-p", :type => :string, 449 | :desc => "push to suite immediately after building" 450 | method_option :force, :aliases => "-f", :type => :boolean, 451 | :desc => "force build even when no changes have been made" 452 | def build(pkg_name) 453 | repo = get_repo_handle 454 | 455 | force = false 456 | force = options["force"] if options.has_key? "force" 457 | 458 | branch = nil 459 | branch = options["branch"] if options.has_key? "branch" 460 | 461 | pkg = repo.get_package pkg_name 462 | version = pkg.build branch, force 463 | 464 | unless version 465 | log :warn, "Build stopped (add -f to build anyway)" 466 | return 467 | end 468 | 469 | if options["push"] && version 470 | if options["push"] == "push" 471 | repo.push pkg.name, version, "testing" # FIXME: should be configurable 472 | else 473 | if repo.codename_to_suite(options["push"]) == 'stable-security' 474 | raise "Package built, but can't push to #{options["push"]}, use the 'release-security' subcommand" 475 | end 476 | repo.push pkg.name, version, options["push"] 477 | end 478 | end 479 | end 480 | 481 | 482 | desc "push PKG-NAME", "push a built package to a specified suite" 483 | method_option :suite, :aliases => "-s", :type => :string, 484 | :desc => "the target suite (defaults to testing)" 485 | method_option :build, :aliases => "-b", :type => :string, 486 | :desc => "which version to push (defaults to the highest one build)" 487 | method_option :force, :aliases => "-f", :type => :boolean, 488 | :desc => "force inclusion of the package to the suite" 489 | def push(pkg_name) 490 | repo = get_repo_handle 491 | 492 | suite = nil 493 | suite = options["suite"] if options.has_key? "suite" 494 | 495 | if repo.codename_to_suite(options["suite"]) == 'stable-security' 496 | raise "Can't push package to #{options["suite"]}, please use the 'release-security' subcommand" 497 | end 498 | 499 | version = nil 500 | version = options["build"] if options.has_key? "build" 501 | 502 | repo.push pkg_name, version, suite, options["force"] == true 503 | end 504 | 505 | 506 | desc "unpush PKG-NAME SUITE", "remove a built package from a suite" 507 | def unpush(pkg_name, suite) 508 | repo = get_repo_handle 509 | repo.unpush pkg_name, suite 510 | end 511 | 512 | desc "list SUBCOMMAND [ARGS]", "show information about packages" 513 | map "l" => :list, "ls" => :list 514 | subcommand "list", List 515 | 516 | 517 | desc "config SUBCOMMAND [ARGS]", "configure your repository" 518 | map "c" => :config, "conf" => :config, "configure" => :config 519 | subcommand "config", Conf 520 | 521 | desc "rm [pkg-name]", "remove a package completely from the build system" 522 | method_option :force, :aliases => "-f", :type => :boolean, 523 | :desc => "force removal even if the package is still used" 524 | def rm(pkg_name) 525 | repo = get_repo_handle 526 | repo.remove pkg_name, options["force"] == true 527 | end 528 | 529 | 530 | desc "rmbuild PKG-NAME VERSION", "remove a built version of a package" 531 | method_option :force, :aliases => "-f", :type => :boolean, 532 | :desc => "force removal even if the build is still used" 533 | def rmbuild(pkg_name, version) 534 | repo = get_repo_handle 535 | repo.remove_build pkg_name, version, options["force"] == true 536 | end 537 | 538 | desc "update [SUITE]", "Update and rebuild (if necessary) all the packages in the suite" 539 | method_option :branch, :aliases => "-b", :type => :string, 540 | :desc => "Branch to use as the source for update" 541 | def update(suite="testing") 542 | log :info, "Updating all packages in the #{suite.fg "blue"} suite" 543 | repo = get_repo_handle 544 | branch = options["branch"] if options.has_key? "branch" 545 | 546 | updated = 0 547 | repo.list_packages(suite).each do |pkg| 548 | log :info, "Updating #{pkg.name.style "pkg-name"}" 549 | begin 550 | version = pkg.build branch 551 | rescue Dr::Package::UnableToBuild 552 | log :info, "" 553 | next 554 | rescue Exception => e 555 | # Handle all other exceptions and try to build next package 556 | log :err, e.to_s 557 | log :info, "" 558 | next 559 | end 560 | 561 | if version && !repo.suite_has_higher_pkg_version?(suite, pkg, version) 562 | repo.push pkg.name, version, suite 563 | updated += 1 564 | end 565 | 566 | log :info, "" 567 | end 568 | 569 | log :info, "Updated #{updated.to_s.fg "blue"} packages in #{suite.fg "blue"}" 570 | end 571 | 572 | 573 | desc "git-tag-release TAG", "Mark relased packages' repositories" 574 | method_option :force, :aliases => "-f", :type => :boolean, 575 | :desc => "Force override existing tags" 576 | method_option :package, :aliases => "-p", :type => :string, 577 | :desc => "Only tag a single package" 578 | method_option :summary, :aliases => "-s", :type => :string, 579 | :desc => "A summary for the release (Github only)" 580 | method_option :title, :aliases => "-t", :type => :string, 581 | :desc => "A title for the release (Github only)" 582 | def git_tag_release(tag) 583 | repo = get_repo_handle 584 | 585 | packages = if options["package"] == nil 586 | repo.list_packages "stable" 587 | else 588 | if repo.get_subpackage_versions(options["package"])["stable"].empty? 589 | log :warn, "This package isn't in the #{"stable".fg "green"} branch, skipping." 590 | end 591 | 592 | [repo.get_package(options["package"])] 593 | end 594 | 595 | packages.each do |pkg| 596 | if pkg.is_a? Dr::GitPackage 597 | version = repo.get_subpackage_versions(pkg.name)["stable"].values.max 598 | bm = repo.get_build_metadata pkg.name, version 599 | 600 | pkg.tag_release tag, bm["revision"], options 601 | else 602 | log :info, "#{pkg.name.style "pkg-name"} is not associated with a git repo, skipping" 603 | end 604 | end 605 | end 606 | 607 | desc "git-print-hashes", "Print the hashes for each repo in release" 608 | method_option :package, :aliases => "-p", :type => :string, 609 | :desc => "Only hash a single package" 610 | def git_print_hashes() 611 | repo = get_repo_handle 612 | 613 | packages = if options["package"] == nil 614 | repo.list_packages "stable" 615 | else 616 | if repo.get_subpackage_versions(options["package"])["stable"].empty? 617 | log :warn, "This package isn't in the #{"stable".fg "green"} branch, skipping." 618 | end 619 | 620 | [repo.get_package(options["package"])] 621 | end 622 | 623 | packages.each do |pkg| 624 | if pkg.is_a? Dr::GitPackage 625 | version = repo.get_subpackage_versions(pkg.name)["stable"].values.max 626 | if pkg.build_exists? version 627 | bm = repo.get_build_metadata pkg.name, version 628 | log :info, "#{pkg.get_repo_url} #{bm["revision"]}" 629 | else 630 | log :err, "#{pkg.get_repo_url} missing build version #{version}" 631 | end 632 | else 633 | log :info, "#{pkg.name.style "pkg-name"} is not associated with a git repo, skipping" 634 | end 635 | end 636 | end 637 | 638 | 639 | desc "release RC-SUITE [DEST-SUITE]", "Push all the packages from RC-SUITE to DEST-SUITE (to release by default)" 640 | method_option :force, :aliases => "-f", :type => :boolean, 641 | :desc => "Force-push all released packages" 642 | def release(rc_suite=nil, dest_suite="release") 643 | if rc_suite.nil? 644 | log :err, "#{"DEPRECATED".fg('red')}, suite required. Doing nothing." 645 | log :err, "Use '#{"dr release RC-SUITE".fg("yellow")}' instead." 646 | raise "Running '#{"dr release".fg("yellow")}' command without suite deprecated" 647 | end 648 | 649 | repo = get_repo_handle 650 | suite = repo.codename_to_suite rc_suite 651 | 652 | if suite.nil? 653 | log :err, "Suite '#{rc_suite.fg("yellow")}' not found" 654 | raise "Suite '#{rc_suite.fg("yellow")}' doesn't exist in the repo" 655 | end 656 | 657 | release_codename = dest_suite 658 | release_suite = repo.codename_to_suite release_codename 659 | 660 | log :info, "Pushing packages from #{rc_suite.fg("yellow")} to #{release_codename.fg("yellow")}" 661 | suite_diff rc_suite, release_codename 662 | prompt_to_confirm "Are you sure you want to continue?", "Aborting release" 663 | 664 | log :info, "Releasing all packages from testing" 665 | repo.list_packages(suite).each do |pkg| 666 | v = repo.get_subpackage_versions(pkg.name)[suite].values 667 | begin 668 | repo.push pkg.name, v.max, release_suite, (options["force"] == true) 669 | rescue Dr::AlreadyExists 670 | ; 671 | end 672 | end 673 | 674 | log :info, "Removing packages that are not in #{rc_suite} any more" 675 | repo.list_packages(release_codename).each do |pkg| 676 | if ! repo.suite_has_package? suite, pkg.name 677 | repo.unpush pkg.name, release_codename 678 | end 679 | end 680 | end 681 | 682 | 683 | desc "release-security PKG-NAME", "Push a built package from testing to 'stable-security' suite" 684 | method_option :force, :aliases => "-f", :type => :boolean, 685 | :desc => "Force-push the released package" 686 | def release_security(pkg_name) 687 | repo = get_repo_handle 688 | 689 | suite_source = 'testing' 690 | 691 | log :info, "Releasing pkg #{pkg_name.style "pkg-name"} package from '#{suite_source}' to 'stable-security'" 692 | 693 | if !repo.suite_has_package? suite_source, pkg_name 694 | log :err, "Package #{pkg_name.style "pkg-name"} not in '#{suite_source}'" 695 | raise "Package #{pkg_name.style "pkg-name"} doesn't exist in the repo" 696 | end 697 | 698 | prompt_msg = "Are you absolutely sure you want to push package #{pkg_name.style "pkg-name"} to 'stable-security'?" 699 | negative_msg = "Couldn't confirm for releasing to stable-security" 700 | prompt_to_confirm prompt_msg, negative_msg 701 | 702 | version = repo.get_subpackage_versions(pkg_name)[suite_source].values.max 703 | log :info, "Package version #{version.style "version"} found in '#{suite_source}'" 704 | begin 705 | repo.push pkg_name, version, "stable-security", (options["force"] == true) 706 | rescue Dr::AlreadyExists 707 | ; 708 | end 709 | end 710 | 711 | desc "suite-diff SUITE OTHER-SUITE", "Show the differences between packages in two suites" 712 | method_option :deb, :aliases => "-d", :type => :boolean, 713 | :desc => "Check only deb packages" 714 | def suite_diff(first, second) 715 | repo = get_repo_handle 716 | 717 | first = repo.codename_to_suite first 718 | if !first 719 | log :err, "Can't find the #{first.fg 'blue'} suite in this repo" 720 | raise "Suite doesn't exist" 721 | end 722 | 723 | second = repo.codename_to_suite second 724 | if !second 725 | log :err, "Can't find the #{second.fg 'blue'} suite in this repo" 726 | raise "Suite doesn't exist" 727 | end 728 | 729 | log :info, "Showing the differences between #{first.fg 'green'} and #{second.fg 'red'}" 730 | if options["deb"] 731 | log :info, "Only for Deb packages" 732 | end 733 | 734 | thread_pool repo.list_packages(first) do |pkg| 735 | if options["deb"] and not pkg.is_a? Dr::DebPackage 736 | next 737 | end 738 | 739 | subpackage_versions = repo.get_subpackage_versions pkg.name 740 | 741 | first_v = subpackage_versions[first].values.max 742 | if !repo.suite_has_package? second, pkg.name 743 | log :info, "#{pkg.name.fg 'orange'} is in #{first.fg 'green'} but not in #{second.fg 'red'}" 744 | next 745 | end 746 | 747 | second_v = subpackage_versions[second].values.max 748 | if Dr::PkgVersion.new(first_v) != Dr::PkgVersion.new(second_v) 749 | log :info, "#{pkg.name.fg 'orange'} #{first_v.fg 'green'} != #{second_v.fg 'red'}" 750 | end 751 | end 752 | 753 | thread_pool repo.list_packages(second) do |pkg| 754 | if !repo.suite_has_package? first, pkg.name 755 | log :info, "#{pkg.name.fg 'orange'} is in #{second.fg 'red'} but not in #{first.fg 'green'}" 756 | end 757 | end 758 | end 759 | 760 | 761 | desc "force-sync PKG-NAME", "Force cloning the sources repository from scratch again" 762 | method_option :url, :aliases => "-u", :type => :string, 763 | :desc => "The URL to clone from" 764 | method_option :branch, :aliases => "-b", :type => :string, 765 | :desc => "The default branch to use for building" 766 | def force_sync(pkg_name) 767 | repo = get_repo_handle 768 | pkg = repo.get_package pkg_name 769 | 770 | if pkg.is_a? Dr::GitPackage 771 | pkg.reinitialise_repo options["url"], options["branch"] 772 | else 773 | raise "The source of #{pkg_name.style "pkg-name"} is not managed by " + 774 | "#{"dr".bright}" 775 | end 776 | end 777 | 778 | #desc "snapshot", "save a snapshot of the archive" 779 | #def snapshot(tag) 780 | # repo = get_repo_handle 781 | #end 782 | 783 | desc "cleanup", "Remove builds beyond certain date or number" 784 | method_option :package, :aliases => "-p", :type => :string, 785 | :desc => "Cleanup this package only" 786 | method_option :date, :aliases => "-d", :type => :string, 787 | :desc => "Remove builds beyond this date (YYYYMMDD)" 788 | method_option :number, :aliases => "-n", :type => :string, 789 | :desc => "Keep only N newest builds" 790 | def cleanup 791 | repo = get_repo_handle 792 | 793 | if options["date"] != nil && options["number"] != nil 794 | log :err, "Can't use -n and -d at the same time" 795 | raise "Bad arguments" 796 | end 797 | 798 | date = options["date"] 799 | number = options["number"] 800 | 801 | if options["date"] == nil && options["number"] == nil 802 | number = 10 803 | end 804 | 805 | packages = unless options["package"] == nil 806 | [repo.get_package(options["package"])] 807 | else 808 | repo.list_packages 809 | end 810 | 811 | packages.each do |pkg| 812 | kept = 0 813 | pkg.history.each do |version_string| 814 | # Can't remove a used build 815 | if repo.is_used? pkg.name, version_string 816 | kept += 1 817 | next 818 | end 819 | 820 | if date != nil 821 | version = Dr::PkgVersion.new version_string 822 | if version.date.to_i < date.to_i 823 | rmbuild pkg.name, version_string 824 | end 825 | elsif number != nil && kept >= number.to_i 826 | rmbuild pkg.name, version_string 827 | else 828 | kept += 1 829 | end 830 | end 831 | end 832 | end 833 | 834 | desc "serve", "Start the archive server" 835 | method_option :port, :aliases => "-p", :type => :numeric, 836 | :desc => "The port to run the server on", :default => 80 837 | method_option :bind, :aliases => "-b", :type => :string, 838 | :desc => "Address to listen on", :default => "0.0.0.0" 839 | method_option :route, :aliases => "-R", :type => :string, 840 | :desc => "The route to serve the archive on", :default => "/" 841 | def serve 842 | repo = get_repo_handle 843 | s = Dr::Server.new options["port"], options["route"], 844 | options["bind"], repo.get_archive_path 845 | s.start 846 | end 847 | 848 | private 849 | def prompt_to_confirm(prompt_msg, negative_message) 850 | response = 'x' 851 | while ! ['y', 'n'].include? response 852 | print prompt_msg 853 | print "[y/n]: " 854 | response = STDIN.gets.strip.downcase 855 | if response == 'n' 856 | log :err, "Replied negatively to prompt, aborting..." 857 | raise negative_message 858 | elsif response == 'y' 859 | log :info, "Received confirmation, will carry on" 860 | else 861 | print "Not an acceptable answer, please answer y/n\n" 862 | end 863 | end 864 | end 865 | 866 | end 867 | 868 | 869 | begin 870 | Dr::check_dependencies [ 871 | "git", "reprepro", "gzip", "debuild", "debootstrap", "qemu-*-static", 872 | "chroot", "curl", "gpg", "tar", "dpkg", "dpkg-deb", "dpkg-sig", "rm", 873 | "sudo" 874 | ] 875 | 876 | RepoCLI.start ARGV 877 | rescue StandardError => e 878 | Dr::Logger.log :err, e.to_s 879 | e.backtrace.each do |line| 880 | line = " #{line}" if line.length > 0 && line[0] == '/' 881 | Dr::Logger.log :err, line.fg("grey") 882 | end 883 | end 884 | -------------------------------------------------------------------------------- /dr.conf-example: -------------------------------------------------------------------------------- 1 | default_repo: "production" 2 | 3 | repositories: 4 | - name: "production" 5 | location: "/path/to/the/production/repo" 6 | 7 | - name: "backup" 8 | location: "/path/to/the/backup/repo" 9 | 10 | - name: "sandbox" 11 | location: "/path/to/the/sandbox/repo" 12 | 13 | build_environments: 14 | basic_wheezy: 15 | name: "Test Build Environment" 16 | arches: [armhf, x86_64] 17 | repos: 18 | wheezy: 19 | url: "http://ftp.uk.debian.org/debian/" 20 | key: "https://ftp-master.debian.org/keys/archive-key-7.0.asc" 21 | src: false 22 | codename: wheezy 23 | components: main nonfree 24 | base_repo: wheezy 25 | packages: [] 26 | 27 | -------------------------------------------------------------------------------- /dr.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'dr/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "dr" 8 | spec.version = Dr::VERSION 9 | spec.authors = ["Radek Pazdera"] 10 | spec.email = ["radek@kano.me"] 11 | spec.summary = %q{dr stands for debian-repository. It is a packaging 12 | tool that helps you make, distribute and maintain 13 | you own disto packages and repositories. It's in a 14 | very early stage, NOT READY for production.} 15 | spec.description = %q{dr works with distribution-level packaging 16 | tools and helps you make and distribute your own 17 | Debian packages through your own repository. 18 | This is a super early release, certainly NOT ready 19 | for production.} 20 | spec.homepage = "http://github.com/KanoComputing/kano-package-system" 21 | spec.license = "GPLv2" 22 | 23 | spec.files = `git ls-files`.split($/) 24 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 25 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 26 | spec.require_paths = ["lib"] 27 | 28 | spec.add_dependency "thor", "~> 0.18", "< 0.19.2" 29 | spec.add_dependency "tco", "~> 0.1" 30 | spec.add_dependency "octokit", "~> 3.3" 31 | spec.add_dependency "rack", "~> 1.6", ">= 1.6.4" 32 | spec.add_dependency "thin", "~> 1.6", ">= 1.6.3" 33 | 34 | spec.add_development_dependency "bundler", "~> 2.0", ">= 2.0.1" 35 | spec.add_development_dependency "rake", "~> 10.3" 36 | spec.add_development_dependency "rspec", "~> 3.1" 37 | end 38 | -------------------------------------------------------------------------------- /lib/dr.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Kano Computing Ltd. 2 | # License: http://www.gnu.org/licenses/gpl-2.0.txt GNU General Public License v2 3 | 4 | require "dr/version" 5 | require "dr/repo" 6 | require "dr/logger" 7 | 8 | module Dr 9 | def self.check_dependencies(deps=[]) 10 | # TODO: /usr/sbin is hacked in because we're using sudo 11 | (ENV["PATH"].split(File::PATH_SEPARATOR) + ["/usr/sbin/"]).each do |path_dir| 12 | deps.delete_if do |dep_name| 13 | Dir[File.join(path_dir, dep_name)].length > 0 14 | end 15 | end 16 | 17 | if deps.length > 0 18 | Logger.log :warn, "Missing some dependencies:" 19 | deps.each { |dep| Logger.log :warn, " #{dep.fg "red"}" } 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/dr/build_environments.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2019 Kano Computing Ltd. 2 | # License: http://www.gnu.org/licenses/gpl-2.0.txt GNU GPL v2 3 | 4 | module Dr 5 | module BuildEnvironments 6 | @@build_environments = { 7 | :kano => { 8 | :name =>"Kano OS (Wheezy)", 9 | :arches => ["armhf"], 10 | :repos => { 11 | :raspbian => { 12 | :url => "http://www.mirrorservice.org/sites/archive.raspbian.org/raspbian/", 13 | :key => "http://www.mirrorservice.org/sites/archive.raspbian.org/raspbian.public.key", 14 | :src => true, 15 | :codename => "wheezy", 16 | :components => "main contrib non-free rpi" 17 | }, 18 | 19 | :raspi_foundation => { 20 | :url => "http://dev.kano.me/mirrors/raspberrypi/", 21 | :key => "http://dev.kano.me/mirrors/raspberrypi/raspberrypi.gpg.key", 22 | :src => false, 23 | :codename => "wheezy", 24 | :components => "main" 25 | }, 26 | 27 | :kano => { 28 | :url => "http://dev.kano.me/archive/", 29 | :key => "http://dev.kano.me/archive/repo.gpg.key", 30 | :src => false, 31 | :codename => "devel", 32 | :components => "main" 33 | } 34 | }, 35 | :base_repo => :raspbian, 36 | :packages => [] 37 | }, 38 | 39 | :kano_stretch => { 40 | :name =>"Kano OS (Stretch)", 41 | :arches => ["armhf"], 42 | :repos => { 43 | :stretch_bootstrap => { 44 | # This is used to debootstrap the system which suffers from the 45 | # problem that S3 doesn't like serving URLs with `+` in the path 46 | # so use the proxied version of: 47 | # staging.stretch.raspbian.repo.os.kano.me 48 | :url => "http://build.os.kano.me/", 49 | :key => "http://build.os.kano.me/raspbian.public.key", 50 | :src => true, 51 | :codename => "stretch", 52 | :components => "main contrib non-free rpi", 53 | :build_only => true 54 | }, 55 | :raspbian_stretch => { 56 | :url => "http://staging.stretch.raspbian.repo.os.kano.me/", 57 | :key => "http://staging.stretch.raspbian.repo.os.kano.me/raspbian.public.key", 58 | :src => true, 59 | :codename => "stretch", 60 | :components => "main contrib non-free rpi" 61 | }, 62 | 63 | :raspi_foundation_stretch => { 64 | :url => "http://dev.kano.me/raspberrypi-stretch/", 65 | :key => "http://dev.kano.me/raspberrypi-stretch/raspberrypi.gpg.key", 66 | :src => false, 67 | :codename => "stretch", 68 | :components => "main" 69 | }, 70 | 71 | :kano_stretch => { 72 | :url => "http://dev.kano.me/archive-stretch/", 73 | :key => "http://dev.kano.me/archive-stretch/repo.gpg.key", 74 | :src => false, 75 | :codename => "devel", 76 | :components => "main" 77 | } 78 | }, 79 | :base_repo => :stretch_bootstrap, 80 | :packages => [] 81 | }, 82 | 83 | :kano_jessie => { 84 | :name =>"Kano OS (Jessie)", 85 | :arches => ["armhf"], 86 | :repos => { 87 | :raspbian_jessie => { 88 | :url => "http://staging.jessie.raspbian.repo.os.kano.me/", 89 | :key => "http://staging.jessie.raspbian.repo.os.kano.me/raspbian.public.key", 90 | :src => true, 91 | :codename => "jessie", 92 | :components => "main contrib non-free rpi" 93 | }, 94 | 95 | :raspi_foundation_jessie => { 96 | :url => "http://dev.kano.me/raspberrypi-jessie/", 97 | :key => "http://dev.kano.me/raspberrypi-jessie/raspberrypi.gpg.key", 98 | :src => false, 99 | :codename => "jessie", 100 | :components => "main" 101 | }, 102 | 103 | :kano_jessie => { 104 | :url => "http://dev.kano.me/archive-jessie/", 105 | :key => "http://dev.kano.me/archive-jessie/repo.gpg.key", 106 | :src => false, 107 | :codename => "devel", 108 | :components => "main" 109 | } 110 | }, 111 | :base_repo => :raspbian_jessie, 112 | :packages => [] 113 | }, 114 | 115 | :wheezy => { 116 | :name => "Debian Wheezy", 117 | :arches => ["x86_64"], 118 | :repos => { 119 | :wheezy => { 120 | :url => "http://ftp.uk.debian.org/debian/", 121 | :key => "https://ftp-master.debian.org/keys/archive-key-7.0.asc", 122 | :src => true, 123 | :codename => "wheezy", 124 | :components => "main contrib non-free" 125 | } 126 | }, 127 | :base_repo => :wheezy, 128 | :packages => [] 129 | }, 130 | 131 | :jessie => { 132 | :name => "Debian Jessie", 133 | :arches => ["x86_64"], 134 | :repos => { 135 | :wheezy => { 136 | :url => "http://ftp.uk.debian.org/debian/", 137 | :key => "https://ftp-master.debian.org/keys/archive-key-8.asc", 138 | :src => true, 139 | :codename => "jessie", 140 | :components => "main contrib non-free" 141 | } 142 | }, 143 | :base_repo => :wheezy, 144 | :packages => [] 145 | } 146 | } 147 | 148 | def build_environments 149 | @@build_environments 150 | end 151 | 152 | def add_build_environment(name, benv) 153 | @@build_environments[name.to_sym] = benv 154 | end 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /lib/dr/buildroot.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2019 Kano Computing Ltd. 2 | # License: http://www.gnu.org/licenses/gpl-2.0.txt GNU GPL v2 3 | 4 | require "tco" 5 | 6 | require "dr/logger" 7 | require "dr/shellcmd" 8 | require "dr/config" 9 | 10 | module Dr 11 | class BuildRoot 12 | include Logger 13 | 14 | def initialize(env, arch, br_cache) 15 | @arch = arch 16 | if arch == "all" 17 | unless Dr::config.build_environments.has_key? env 18 | log :err, "Unkown build environment: #{env.to_s.fg "red"}" 19 | raise "Build environment not recognised" 20 | end 21 | @arch = Dr::config.build_environments[env][:arches][0] 22 | end 23 | 24 | @location = "#{br_cache}/#{env}-#{@arch}.tar.gz" 25 | @env = env 26 | 27 | @essential_pkgs = "sudo,vim,ca-certificates,fakeroot,build-essential," + 28 | "curl,devscripts,debhelper,git,bc,locales,equivs," + 29 | "pkg-config,libfile-fcntllock-perl" 30 | 31 | unless File.exist? @location 32 | setup @env, @arch 33 | end 34 | end 35 | 36 | def open 37 | Dir.mktmpdir do |tmp| 38 | log :info, "Preparing #{@env.to_s.fg "blue"} #{@arch.fg "orange"} build root" 39 | log :info, "Sysroot #{@location}" 40 | ShellCmd.new "sudo tar xz -C #{tmp} -f #{@location}", :tag => "tar" 41 | begin 42 | log :info, "Mounting the /proc file system" 43 | mnt_cmd = "sudo chroot #{tmp} mount -t proc none /proc" 44 | ShellCmd.new mnt_cmd, :tag => "mount" 45 | 46 | log :info, "Mounting /dev/urandom" 47 | touch_cmd = "sudo touch #{tmp}/dev/urandom" 48 | ShellCmd.new touch_cmd, :tag => "touch" 49 | mnt2_cmd = "sudo mount --bind /dev/urandom #{tmp}/dev/urandom" 50 | ShellCmd.new mnt2_cmd, :tag => "mount2" 51 | 52 | keys_cmd = "sudo cp -r ~/.ssh #{tmp}/root/" 53 | ShellCmd.new keys_cmd, :tag => "SSH keys copy" 54 | 55 | yield tmp 56 | ensure 57 | log :info, "Unmounting the /proc file system" 58 | umnt_cmd = "sudo chroot #{tmp} umount -f /proc" 59 | ShellCmd.new umnt_cmd, :tag => "umount" 60 | 61 | log :info, "Unmounting the /dev/urandom " 62 | umnt2_cmd = "sudo umount -f #{tmp}/dev/urandom" 63 | ShellCmd.new umnt2_cmd, :tag => "umount" 64 | 65 | log :info, "Cleaning up the buildroot" 66 | ShellCmd.new "sudo rm -rf #{tmp}/*", :tag => "rm" 67 | end 68 | end 69 | end 70 | 71 | private 72 | def setup(env, arch) 73 | unless Dr.config.build_environments.has_key? env 74 | raise "Sorry, build environment #{env.to_s.fg "blue"} isn't supported by dr." 75 | end 76 | 77 | unless Dr.config.build_environments[env][:arches].include? arch 78 | raise "Arch #{arch.fg "blue"} not supported by this build environment." 79 | end 80 | 81 | repos = Dr.config.build_environments[env][:repos] 82 | 83 | Dir.mktmpdir do |tmp| 84 | broot = "#{tmp}/broot" 85 | FileUtils.mkdir_p "#{tmp}/broot" 86 | 87 | log :info, "Setting up the buildroot" 88 | 89 | begin 90 | arch_cmd = ShellCmd.new "arch" 91 | arch = arch_cmd.out.strip 92 | 93 | if @arch == arch 94 | setup_native broot 95 | else 96 | setup_foreign broot 97 | end 98 | 99 | log :info, "Configuring the build root" 100 | 101 | repo_setup_sequences = repos.reject { |name, repo| 102 | repo.key? :build_only and repo[:build_only] 103 | }.map do |name, repo| 104 | seq = "echo 'deb #{repo[:url]} #{repo[:codename]} " + 105 | "#{repo[:components]}' >> /etc/apt/sources.list\n" 106 | 107 | if repo.has_key?(:src) && repo[:src] 108 | seq += "echo 'deb-src #{repo[:url]} #{repo[:codename]} " + 109 | "#{repo[:components]}' >> /etc/apt/sources.list\n" 110 | end 111 | 112 | if repo.has_key?(:key) && repo[:key] 113 | seq += "curl --retry 5 '#{repo[:key]}' | apt-key add -\n" 114 | end 115 | 116 | seq 117 | end 118 | 119 | cmd = "sudo chroot #{broot} </etc/locale.gen 124 | locale-gen en_US.UTF-8 125 | 126 | cat >>/etc/bash.bashrc < "chroot" 134 | 135 | log :info, "Updating package lists" 136 | update = ShellCmd.new "sudo chroot #{broot} apt-get update", { 137 | :tag => "chroot", 138 | :show_out => true 139 | } 140 | 141 | # TODO: is this necessary? 142 | #Kernel.system "sudo chroot #{broot} useradd -m -s /bin/bash raspbian" 143 | 144 | log :info, "Creating the build root archive" 145 | cmd = "sudo tar cz -C #{broot} -f #{@location} `ls -1 #{broot}`" 146 | tar = ShellCmd.new cmd, :tag => "tar" 147 | ensure 148 | log :info, "Cleaning up" 149 | ShellCmd.new "sudo rm -rf #{broot}", :tag => "rm" 150 | end 151 | end 152 | end 153 | 154 | def setup_foreign(broot) 155 | repos = Dr.config.build_environments[@env][:repos] 156 | base_repo = Dr.config.build_environments[@env][:base_repo].to_sym 157 | additional_pkgs = Dr.config.build_environments[@env][:packages].join "," 158 | codename = repos[base_repo][:codename] 159 | url = repos[base_repo][:url] 160 | 161 | log :info, "Bootstrapping #{@env} (foreign chroot, first stage)" 162 | cmd = "sudo debootstrap --foreign --variant=buildd --no-check-gpg " + 163 | "--include=#{@essential_pkgs},#{additional_pkgs} " + 164 | "--arch=#{@arch} #{codename} #{broot} #{url}" 165 | debootsrap = ShellCmd.new cmd, { 166 | :tag => "debootstrap", 167 | :show_out => true 168 | } 169 | 170 | static_qemu = Dir["/usr/bin/qemu-*-static"] 171 | static_qemu.each do |path| 172 | cp = ShellCmd.new "sudo cp #{path} #{broot}/usr/bin", { 173 | :tag => "cp" 174 | } 175 | end 176 | 177 | log :info, "Bootstrapping Raspian (#{@arch} stage)" 178 | cmd = "sudo chroot #{broot} /debootstrap/debootstrap --second-stage" 179 | debootstrap = ShellCmd.new cmd, { 180 | :tag => "debootstrap", 181 | :show_out => true 182 | } 183 | end 184 | 185 | def setup_native(broot) 186 | repos = Dr.config.build_environments[@env][:repos] 187 | base_repo = Dr.config.build_environments[@env][:base_repo].to_sym 188 | additional_pkgs = Dr.config.build_environments[@env][:packages].join "," 189 | codename = repos[base_repo][:codename] 190 | url = repos[base_repo][:url] 191 | 192 | log :info, "Bootstrapping #{@env} (native chroot)" 193 | cmd = "sudo debootstrap --variant=buildd --no-check-gpg " + 194 | "--include=#{@essential_pkgs},#{additional_pkgs} " + 195 | "#{codename} #{broot} #{repos[base_repo][:url]}" 196 | debootsrap = ShellCmd.new cmd, { 197 | :tag => "debootstrap", 198 | :show_out => true 199 | } 200 | end 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /lib/dr/config.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Kano Computing Ltd. 2 | # License: http://www.gnu.org/licenses/gpl-2.0.txt GNU General Public License v2 3 | 4 | require "yaml" 5 | 6 | require "dr/build_environments" 7 | 8 | module Dr 9 | class Config 10 | attr_reader :default_repo, :repositories 11 | 12 | include BuildEnvironments 13 | 14 | def initialize(locations) 15 | @default_repo = nil 16 | @repositories = {} 17 | 18 | locations.each do |conf_file| 19 | conf_file = File.expand_path conf_file 20 | next unless File.exist? conf_file 21 | load conf_file 22 | end 23 | end 24 | 25 | private 26 | def load(path) 27 | conf_file = YAML::load_file path 28 | 29 | if conf_file.has_key? "repositories" 30 | if conf_file["repositories"].is_a? Array 31 | conf_file["repositories"].each do |repo| 32 | raise "Repo name missing in the config." unless repo.has_key? "name" 33 | raise "Repo location missing in the config" unless repo.has_key? "location" 34 | @repositories[repo["name"]] = { 35 | :location => repo["location"] 36 | } 37 | end 38 | else 39 | raise "The 'repositories' config option must be an array." 40 | end 41 | end 42 | 43 | if conf_file.has_key? "default_repo" 44 | @default_repo = conf_file["default_repo"] 45 | unless @repositories.has_key? @default_repo 46 | raise "Default repo #{@default_repo} doesn't exist" 47 | end 48 | end 49 | 50 | if conf_file.has_key? "build_environments" 51 | conf_file["build_environments"].each do |id, be| 52 | be_sym_keys = be.inject({}) { |memo,(k,v)| memo[k.to_sym] = v; memo } 53 | add_build_environment(id, be_sym_keys) 54 | end 55 | end 56 | end 57 | end 58 | 59 | @config = Config.new ["/etc/dr.conf", "~/.dr.conf"] 60 | def self.config 61 | @config 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/dr/debpackage.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Kano Computing Ltd. 2 | # License: http://www.gnu.org/licenses/gpl-2.0.txt GNU General Public License v2 3 | 4 | require "dr/package" 5 | 6 | module Dr 7 | class DebPackage < Package 8 | def self.setup(repo, deb_file, force=false) 9 | dpkg = ShellCmd.new "dpkg-deb --field #{deb_file} Source", :tag => "dpkg" 10 | src_name = dpkg.out.chomp 11 | if src_name == "" 12 | dpkg = ShellCmd.new "dpkg-deb --field #{deb_file} Package", :tag => "dpkg" 13 | src_name = dpkg.out.chomp 14 | end 15 | 16 | deb_file_name = File.basename(deb_file) 17 | log :info, "Adding the #{deb_file_name.style "subpkg-name"} package" 18 | 19 | dpkg = ShellCmd.new "dpkg-deb --field #{deb_file} Version", :tag => "dpkg" 20 | version = dpkg.out.chomp 21 | 22 | deb_dir = "#{repo.location}/packages/#{src_name}/builds/#{version}" 23 | 24 | if File.exist?("#{deb_dir}/#{deb_file_name}") && !force 25 | raise "This deb file is already in the repo" 26 | end 27 | 28 | log :info, "Adding a build to the #{src_name.style "pkg-name"} source package" 29 | FileUtils.mkdir_p deb_dir 30 | FileUtils.cp deb_file.to_s, "#{deb_dir}/" 31 | 32 | log :info, "Signing the deb file" 33 | repo.sign_deb "#{deb_dir}/#{deb_file_name}" 34 | end 35 | 36 | def initialize(name, repo) 37 | super name, repo 38 | end 39 | 40 | def build(branch=nil, force=false) 41 | log :warn, "The sources of the #{@name.style "pkg-name"} package are " + 42 | "not managed by #{"dr".bright}" 43 | raise UnableToBuild.new "Unable to build the package" 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/dr/gitpackage.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Kano Computing Ltd. 2 | # License: http://www.gnu.org/licenses/gpl-2.0.txt GNU General Public License v2 3 | 4 | require "dr/package" 5 | require "dr/pkgversion" 6 | require "dr/shellcmd" 7 | require "dr/utils" 8 | 9 | require "yaml" 10 | require "octokit" 11 | 12 | module Dr 13 | class GitPackage < Package 14 | def self.setup(repo, git_addr, default_branch, force=false) 15 | Dir.mktmpdir do |tmp| 16 | git_cmd = "git clone --mirror --branch #{default_branch} " + 17 | "#{git_addr} #{tmp}/git" 18 | ShellCmd.new git_cmd, :tag => "git", :show_out => true 19 | 20 | FileUtils.mkdir_p "#{tmp}/src" 21 | 22 | log :info, "Extracting the sources" 23 | git_cmd ="git --git-dir #{tmp}/git --bare archive " + 24 | "--format tar #{default_branch} | tar x -C #{tmp}/src" 25 | ShellCmd.new git_cmd, :tag => "git", :show_out => true 26 | 27 | unless File.exist? "#{tmp}/src/debian/control" 28 | log :err, "The debian packaging files not found in the repository" 29 | raise "Adding a package from #{git_addr} failed" 30 | end 31 | 32 | src_name = nil 33 | File.open "#{tmp}/src/debian/control", "r" do |f| 34 | f.each_line do |line| 35 | match = line.match(/^Source: (.+)$/) 36 | if match 37 | src_name = match.captures[0] 38 | break 39 | end 40 | end 41 | end 42 | 43 | unless src_name 44 | log :err, "Couldn't identify the source package" 45 | raise "Adding a package from #{git_addr} failed" 46 | end 47 | 48 | pkg_dir = "#{repo.location}/packages/#{src_name}" 49 | if File.exist? pkg_dir 50 | log :warn, "The package already exists. Add -f to insert it anyway." 51 | raise "Adding failed" 52 | end 53 | 54 | log :info, "Adding #{src_name.style "pkg-name"} to the repository" 55 | FileUtils.mkdir_p pkg_dir.to_s 56 | 57 | log :info, "Setting up builds directory" 58 | FileUtils.mkdir_p "#{pkg_dir}/builds" 59 | 60 | log :info, "Setting up the source directory" 61 | FileUtils.mv "#{tmp}/git", "#{pkg_dir}/source" 62 | 63 | log :info, "The #{src_name.style "pkg-name"} package added successfully" 64 | end 65 | end 66 | 67 | def initialize(name, repo) 68 | super name, repo 69 | 70 | @git_dir = "#{repo.location}/packages/#{name}/source" 71 | @default_branch = get_current_branch 72 | end 73 | 74 | def reinitialise_repo(git_addr=nil, branch=nil) 75 | git_addr ||= get_repo_url 76 | branch ||= @default_branch 77 | 78 | log :info, "Re-downloading the source repository of " + 79 | "#{@name.style "pkg-name"}" 80 | Dir.mktmpdir do |tmp| 81 | git_cmd = "git clone --mirror --branch #{branch} " + 82 | "#{git_addr} #{tmp}/git" 83 | ShellCmd.new git_cmd, :tag => "git", :show_out => true 84 | 85 | src_dir = "#{tmp}/src" 86 | FileUtils.mkdir_p src_dir 87 | 88 | checkout branch, src_dir, "#{tmp}/git" 89 | 90 | unless File.exist? "#{src_dir}/debian/control" 91 | log :err, "The debian packaging files not found in the repository" 92 | raise "Adding a package from #{git_addr} failed" 93 | end 94 | 95 | src_name = nil 96 | File.open "#{tmp}/src/debian/control", "r" do |f| 97 | f.each_line do |line| 98 | match = line.match(/^Source: (.+)$/) 99 | if match 100 | src_name = match.captures[0] 101 | break 102 | end 103 | end 104 | end 105 | 106 | unless src_name 107 | log :err, "Couldn't identify the source package" 108 | raise "Adding a package from #{git_addr} failed" 109 | end 110 | 111 | unless src_name == @name 112 | log :err, "The name of the package in the repo has changed" 113 | raise "Adding a package from #{git_addr} failed" 114 | end 115 | 116 | src_dir = "#{@repo.location}/packages/#{@name}/source" 117 | FileUtils.rm_rf src_dir 118 | FileUtils.mv "#{tmp}/git", src_dir.to_s 119 | end 120 | 121 | @default_branch = branch 122 | end 123 | 124 | def get_configuration 125 | md_file = "#{@repo.location}/packages/#{@name}/metadata" 126 | if File.exist? md_file 127 | Utils::symbolise_keys YAML.load_file md_file 128 | else 129 | {} 130 | end 131 | end 132 | 133 | def set_configuration(config) 134 | # TODO: Some validation needed 135 | md_file = "#{@repo.location}/packages/#{@name}/metadata" 136 | File.open(md_file, "w") do |f| 137 | YAML.dump Utils::stringify_symbols(config), f 138 | end 139 | end 140 | 141 | def build(branch=nil, force=false) 142 | branch = @default_branch unless branch 143 | 144 | version = nil 145 | 146 | orig_rev, curr_rev = update_from_origin branch 147 | 148 | unless curr_rev 149 | log :err, "Branch #{branch.fg "blue"} not found in #{@name.style "pkg-name"}" 150 | raise "The requested branch doesn't exist in the repository!" 151 | end 152 | 153 | log :info, "Branch #{branch.fg "blue"}, revision #{curr_rev[0..7].fg "blue"}" 154 | unless force 155 | history.each do |v| 156 | metadata = @repo.get_build_metadata @name, v 157 | if metadata.has_key?("revision") && metadata["revision"] == curr_rev 158 | msg = "This revision of #{@name.style "pkg-name"} has already " + 159 | "been built and is available as #{v.to_s.style "version"}" 160 | log :info, msg 161 | return v 162 | end 163 | end 164 | end 165 | 166 | Dir.mktmpdir do |src_dir| 167 | checkout branch, src_dir 168 | 169 | version_string = get_version "#{src_dir}/debian/changelog" 170 | unless version_string 171 | log :err, "Couldn't get the version string from the changelog" 172 | raise "The changelog format doesn't seem be right" 173 | end 174 | 175 | version = PkgVersion.new version_string 176 | log :info, "Source version: #{version.source.style "version"}" 177 | 178 | version.add_build_tag 179 | while build_exists? version 180 | version.increment! 181 | end 182 | log :info, "Build version: #{version.to_s.style "version"}" 183 | 184 | log :info, "Updating changelog" 185 | now = Time.new.strftime("%a, %-d %b %Y %T %z") 186 | ch_entry = "#{@name} (#{version}) kano; urgency=low\n" 187 | ch_entry << "\n" 188 | ch_entry << " * Package rebuilt, updated to revision #{curr_rev[0..7]}.\n" 189 | ch_entry << "\n" 190 | ch_entry << " -- Team Kano #{now}\n\n" 191 | 192 | changelog = "" 193 | File.open "#{src_dir}/debian/changelog", "r" do |f| 194 | changelog = f.read 195 | end 196 | 197 | File.open "#{src_dir}/debian/changelog", "w" do |f| 198 | f.write ch_entry 199 | f.write changelog 200 | end 201 | 202 | repo_arches = @repo.get_architectures 203 | pkg_arches = get_architectures("#{src_dir}/debian/control") 204 | arches = case 205 | when pkg_arches.include?("any") 206 | repo_arches 207 | when pkg_arches.include?("all") 208 | ["all"] 209 | else 210 | repo_arches & pkg_arches 211 | end 212 | 213 | if repo_arches.length == 0 214 | log :err, "#{@name.style "pkg-name"} cannot be build for any of " + 215 | "the architectures supported by this repository" 216 | raise "Unable to build the package for this repository" 217 | end 218 | 219 | benv = :default 220 | src_meta = get_configuration 221 | if src_meta.has_key? :build_environment 222 | benv = src_meta[:build_environment].to_sym 223 | end 224 | 225 | if not (arches.map do |arch| @repo.has_arch(arch, benv) end .any?) 226 | log :err, "Could not find any arch supported by this build enviroment" 227 | raise 228 | end 229 | 230 | arches.each do |arch| 231 | if not @repo.has_arch(arch, benv) 232 | log :warn, "Arch #{arch.fg "blue"} not supported by this build environment." 233 | else 234 | 235 | @repo.buildroot(arch, benv).open do |br| 236 | log :info, "Building the #{@name.style "pkg-name"} package " + 237 | "version #{version.to_s.style "version"} for #{arch}" 238 | 239 | # Moving to the proper directory 240 | build_dir_name = "#{@name}-#{version.upstream}" 241 | build_dir = "#{br}/#{build_dir_name}" 242 | FileUtils.cp_r src_dir, build_dir 243 | 244 | # Make orig tarball 245 | all_files = Dir["#{build_dir}/*"] + Dir["#{build_dir}/.*"] 246 | excluded_files = ['.', '..', '.git', 'debian'] 247 | selected_files = all_files.select { |path| !excluded_files.include?(File.basename(path)) } 248 | files = selected_files.map { |f| "\"#{File.basename f}\"" }.join " " 249 | log :info, "Creating orig source tarball" 250 | tar = "tar cz -C #{build_dir} -f #{br}/#{@name}_#{version.upstream}.orig.tar.gz #{files}" 251 | ShellCmd.new tar, :tag => "tar" 252 | 253 | clean = "sudo chroot #{br} apt-get clean" 254 | apt = "sudo chroot #{br} apt-get update" 255 | # FIXME: To install the dependencies, we use both the 256 | # `apt-get build-dep` tool to attempt to download the 257 | # packages first as well as the `mk-build-deps` in case 258 | # the first command fails. Ideally, the first command 259 | # would never fail. 260 | deps = <<-EOS 261 | sudo chroot #{br} < "apt-get-clean", :show_out => true 280 | 281 | log :info, "Updating the sources lists" 282 | ShellCmd.new apt, :tag => "apt-get-update", :show_out => true 283 | 284 | log :info, "Installing build dependencies" 285 | ShellCmd.new deps, :tag => "mk-build-deps", :show_out => true 286 | 287 | log :info, "Building the package" 288 | ShellCmd.new build, :tag => "debuild", :show_out => true 289 | 290 | debs = Dir["#{br}/*.deb"] 291 | expected_pkgs = get_subpackage_names "#{src_dir}/debian/control" 292 | expected_pkgs.each do |subpkg_name| 293 | includes = debs.inject(false) do |r, n| 294 | r || ((/^#{br}\/#{subpkg_name}_#{version.to_s omit_epoch=true}/ =~ n) != nil) 295 | end 296 | 297 | unless includes 298 | log :err, "Subpackage #{subpkg_name} did not build properly: #{debs}" 299 | raise "Building #{name} failed" 300 | end 301 | end 302 | 303 | build_dir = "#{@repo.location}/packages/#{@name}/builds/#{version}" 304 | FileUtils.mkdir_p build_dir 305 | debs.each do |pkg| 306 | FileUtils.cp pkg, build_dir 307 | 308 | deb_filename = File.basename(pkg) 309 | log :info, "Signing the #{deb_filename.style "subpkg-name"} package" 310 | @repo.sign_deb "#{build_dir}/#{deb_filename}" 311 | end 312 | 313 | log :info, "Writing package metadata" 314 | File.open "#{build_dir}/.metadata", "w" do |f| 315 | YAML.dump({"branch" => branch, "revision" => curr_rev}, f) 316 | end 317 | log :info, "The #{@name.style "pkg-name"} package was " + 318 | "built successfully." 319 | end 320 | end 321 | end 322 | end 323 | version 324 | end 325 | 326 | def tag_release(tag_name, revision, options={}) 327 | url = get_repo_url 328 | 329 | log :info, "Tagging #{@name.style "pkg-name"} package for " + 330 | "#{tag_name.fg "yellow"} release" 331 | 332 | gh_repo = case url 333 | when /git\@github.com\:/i then url.split(":")[1].gsub(/\.git$/, "").strip 334 | when /github.com\//i then url.split("/")[-2..-1].join("/").gsub(/\.git$/, "").strip 335 | else nil 336 | end 337 | 338 | if gh_repo == nil 339 | git_cmd = "git --git-dir #{@git_dir} tag #{tag}" 340 | git = ShellCmd.new git_cmd, 341 | :tag => "git", 342 | :show_out => false, 343 | :raise_on_error => false 344 | 345 | if git.status == 128 346 | log :warn, "Tag #{tag_name.fg "yellow"} already exists." 347 | return 348 | end 349 | 350 | git_cmd = "git --git-dir #{@git_dir} push origin --tags" 351 | git = ShellCmd.new git_cmd, :show_out => false 352 | 353 | return 354 | end 355 | 356 | title = options["title"] || "Kano OS #{tag_name}" 357 | summary = options["summary"] || "https://github.com/KanoComputing/peldins/wiki/Changelog-#{tag_name}" 358 | 359 | token = ENV["GITHUB_API_TOKEN"] 360 | client = Octokit::Client.new :access_token => token 361 | 362 | releases = client.releases gh_repo 363 | ri = releases.index { |r| r[:tag_name] == tag_name } 364 | 365 | if ri == nil 366 | client.create_release gh_repo, tag_name, 367 | :target_commitish => revision, 368 | :name => title, 369 | :body => summary 370 | else 371 | log :warn, "The #{tag_name.fg "yellow"} release exists already for #{@name.style "pkg-name"}." 372 | end 373 | end 374 | 375 | def get_repo_name() 376 | url = get_repo_url 377 | b = File.basename(url) 378 | b.slice!(".git") 379 | return b 380 | end 381 | 382 | def get_repo_url 383 | git_cmd = "git --git-dir #{@git_dir} config --get remote.origin.url" 384 | git = ShellCmd.new git_cmd, :tag => "git" 385 | git.out.strip 386 | end 387 | 388 | private 389 | def update_from_origin(branch) 390 | log :info, "Pulling changes from origin" 391 | 392 | original_rev = get_rev branch 393 | 394 | begin 395 | git_cmd = "git --git-dir #{@git_dir} --bare fetch origin" 396 | ShellCmd.new git_cmd, :tag => "git", :show_out => true 397 | rescue Exception => e 398 | log :err, "Unable to pull from origin" 399 | raise e 400 | end 401 | 402 | current_rev = get_rev branch 403 | 404 | [original_rev, current_rev] 405 | end 406 | 407 | def get_version(changelog_file) 408 | File.open changelog_file, "r" do |f| 409 | f.each_line do |l| 410 | version = l.match /^#{@name} \(([^\)]+)\) .+;/ 411 | return version.captures[0] if version 412 | end 413 | end 414 | 415 | nil 416 | end 417 | 418 | def get_subpackage_names(control_file) 419 | packages = [] 420 | File.open control_file, "r" do |f| 421 | f.each_line do |l| 422 | if /^Package: / =~ l 423 | packages.push l.split(" ")[1] 424 | end 425 | end 426 | end 427 | 428 | packages 429 | end 430 | 431 | def get_architectures(control_file) 432 | arches = [] 433 | File.open control_file, "r" do |f| 434 | f.each_line do |l| 435 | m = l.match(/^Architecture: (.+)/) 436 | arches += m.captures[0].chomp.split(" ") if m 437 | end 438 | end 439 | 440 | arches.uniq 441 | end 442 | 443 | def get_current_branch 444 | git_cmd = ShellCmd.new "git --git-dir #{@git_dir} --bare branch", { 445 | :tag => "git" 446 | } 447 | git_cmd.out.chomp.lines.grep(/^\*/)[0][2..-1].chomp 448 | end 449 | 450 | def get_rev(branch) 451 | git_cmd = "git --git-dir #{@git_dir} --bare rev-parse #{branch} 2>/dev/null" 452 | git = ShellCmd.new git_cmd, :tag => "git", :expect => [0, 128] 453 | 454 | if git.status.exitstatus == 0 455 | git.out.chomp 456 | else 457 | nil 458 | end 459 | end 460 | 461 | def checkout(branch, dir, override_git_dir=@git_dir) 462 | log :info, "Extracting the sources" 463 | git_cmd ="git --git-dir #{override_git_dir} --bare archive " + 464 | "--format tar #{branch} | tar x -C #{dir}" 465 | ShellCmd.new git_cmd, :tag => "git", :show_out => true 466 | end 467 | end 468 | end 469 | -------------------------------------------------------------------------------- /lib/dr/gnupg.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Kano Computing Ltd. 2 | # License: http://www.gnu.org/licenses/gpl-2.0.txt GNU General Public License v2 3 | 4 | require "dr/shellcmd" 5 | require "dr/logger" 6 | 7 | module Dr 8 | class GnuPG 9 | include Logger 10 | 11 | def initialize(keyring) 12 | @keyring = keyring 13 | 14 | # initialise the keyring 15 | FileUtils.mkdir_p @keyring 16 | FileUtils.chmod_R 0700, @keyring 17 | end 18 | 19 | def generate_key(name, mail, pass) 20 | #kill_rngd = false 21 | #unless File.exist? "/var/run/rngd.pid" 22 | # print "Starting rngd (root permissions required) ... " 23 | # Kernel.system "sudo rngd -p #{@keyring}/rngd.pid -r /dev/urandom" 24 | # kill_rngd = true 25 | # puts "[OK]" 26 | #end 27 | 28 | log(:info, tag("gpg", "Generating the GPG key")) 29 | 30 | passphrase = "Passphrase: #{pass}" if pass.length > 0 31 | cmd = <<-END 32 | gpg --batch --gen-key --homedir #{@keyring} < "gpg" 46 | 47 | cmd = "gpg --list-keys --with-colons --homedir #{@keyring}" 48 | gpg_cmd = ShellCmd.new cmd, :tag => "gpg" 49 | key_list = gpg_cmd.out.split "\n" 50 | key_entry = key_list.grep(/^pub/).grep(/#{name}/).grep(/#{mail}/) 51 | key = key_entry[0].split(":")[4][8..-1] 52 | 53 | log(:info, tag("gpg", "Key done")) 54 | 55 | #if kill_rngd 56 | # print "Stopping rngd (root permissions required) ... " 57 | # Kernel.system "sudo kill `cat #{@keyring}/rngd.pid`" 58 | # Kernel.system "sudo rm -f #{@keyring}/rngd.pid" 59 | # puts "[OK]" 60 | #end 61 | 62 | key 63 | end 64 | 65 | def get_key_id(key) 66 | cmd = "gpg --homedir #{@keyring} --with-colons --list-public-keys #{key}" 67 | gpg = ShellCmd.new cmd, :tag => "gpg" 68 | 69 | gpg.out.lines.grep(/^pub/)[0].split(":")[9] 70 | end 71 | 72 | def export_pub(key, location) 73 | # TODO: Remove the key before exporting (so gpg doesn't ask about it) 74 | log(:info, tag("gpg", "Exporting key")) 75 | cmd = "gpg --armor --homedir #{@keyring} \ 76 | --output #{location} \ 77 | --export #{key}" 78 | gpg_cmd = ShellCmd.new cmd, :tag => "gpg" 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/dr/logger.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2017 Kano Computing Ltd. 2 | # License: http://www.gnu.org/licenses/gpl-2.0.txt GNU General Public License v2 3 | 4 | require "tco" 5 | require "thread" 6 | 7 | tco_conf = Tco::config 8 | 9 | tco_conf.names["green"] = "#99ad6a" 10 | tco_conf.names["yellow"] = "#d8ad4c" 11 | tco_conf.names["red"] = "#cc333f" #"#cf6a4c" 12 | tco_conf.names["light-grey"] = "#ababab" 13 | tco_conf.names["dark-grey"] = "#2b2b2b" 14 | tco_conf.names["purple"] = "#90559e" 15 | tco_conf.names["blue"] = "#4D9EEB" #"#1b8efa" 16 | tco_conf.names["orange"] = "#ff842a" 17 | tco_conf.names["brown"] = "#6a4a3c" 18 | tco_conf.names["magenta"] = "#ff00ff" 19 | 20 | tco_conf.styles["info"] = { 21 | :fg => "green", 22 | :bg => "dark-grey", 23 | :bright => false, 24 | :underline => false 25 | } 26 | tco_conf.styles["warn"] = { 27 | :fg => "dark-grey", 28 | :bg => "yellow", 29 | :bright => false, 30 | :underline => false 31 | } 32 | tco_conf.styles["err"] = { 33 | :fg => "dark-grey", 34 | :bg => "red", 35 | :bright => false, 36 | :underline => false 37 | } 38 | 39 | tco_conf.styles["debug"] = { 40 | :fg => "light-grey", 41 | :bg => "dark-grey", 42 | :bright => false, 43 | :underline => false 44 | } 45 | 46 | tco_conf.styles["log-head"] = { 47 | :fg => "purple", 48 | :bg => "dark-grey", 49 | :bright => false, 50 | :underline => false 51 | } 52 | 53 | tco_conf.styles["pkg-name"] = { 54 | :fg => "orange", 55 | :bg => "", 56 | :bright => false, 57 | :underline => false 58 | } 59 | 60 | tco_conf.styles["subpkg-name"] = { 61 | :fg => "purple", 62 | :bg => "", 63 | :bright => false, 64 | :underline => false 65 | } 66 | 67 | tco_conf.styles["version"] = { 68 | :fg => "brown", 69 | :bg => "", 70 | :bright => false, 71 | :underline => false 72 | } 73 | 74 | Tco::reconfigure tco_conf 75 | 76 | module Dr 77 | module Logger 78 | @@message_types = { 79 | :info => "info", 80 | :warn => "warn", 81 | :err => "err", 82 | :debug => "debug" 83 | } 84 | 85 | @@verbosity = :verbose 86 | @@logger_verbosity_levels = { 87 | :essential => 0, 88 | :important => 1, 89 | :informative => 2, 90 | :verbose => 3 91 | } 92 | 93 | @@stdout_mutex = Mutex.new 94 | @@log_file = nil 95 | 96 | def self.set_logfile(file) 97 | @@log_file = file 98 | end 99 | 100 | def self.set_verbosity(level) 101 | msg = "Message verbosity level not recognised (#{level})." 102 | raise msg unless @@logger_verbosity_levels.has_key? level.to_sym 103 | 104 | @@verbosity = level.to_sym 105 | end 106 | 107 | def self.log(msg_type, msg, verbosity=nil) 108 | out = "dr".style("log-head") << " " 109 | 110 | case msg_type 111 | when :info 112 | out << "info".style(@@message_types[:info]) 113 | verbosity = :informative unless verbosity 114 | when :warn 115 | out << "WARN".style(@@message_types[:warn]) 116 | verbosity = :informative unless verbosity 117 | when :err 118 | out << "ERR!".style(@@message_types[:err]) 119 | verbosity = :essential unless verbosity 120 | when :debug 121 | out << "dbg?".style(@@message_types[:debug]) 122 | verbosity = :verbose unless verbosity 123 | end 124 | 125 | if verbosity <= @@verbosity 126 | out << " " << msg.chomp 127 | 128 | @@stdout_mutex.synchronize do 129 | puts out 130 | STDOUT.flush 131 | 132 | unless @@log_file.nil? 133 | @@log_file.puts strip_colours out 134 | @@log_file.flush 135 | end 136 | end 137 | end 138 | end 139 | 140 | def log(msg_type, msg) 141 | Logger::log msg_type, msg 142 | end 143 | 144 | def tag(tag, msg) 145 | tag.fg("blue").bg("dark-grey") << " " << msg 146 | end 147 | 148 | private 149 | def self.strip_colours(string) 150 | string.gsub(/\033\[[0-9]+(;[0-9]+){0,2}m/, '') 151 | end 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /lib/dr/package.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2018 Kano Computing Ltd. 2 | # License: http://www.gnu.org/licenses/gpl-2.0.txt GNU General Public License v2 3 | 4 | require "dr/logger" 5 | require "dr/shellcmd" 6 | 7 | module Dr 8 | class Package 9 | class UnableToBuild < RuntimeError 10 | end 11 | 12 | class BuildFailed < RuntimeError 13 | end 14 | 15 | attr_reader :name 16 | 17 | include Logger 18 | class << self 19 | include Logger 20 | end 21 | 22 | def initialize(name, repo) 23 | @name = name 24 | @repo = repo 25 | end 26 | 27 | def history 28 | versions = [] 29 | Dir.foreach "#{@repo.location}/packages/#{name}/builds/" do |v| 30 | versions.push v unless v =~ /^\./ 31 | end 32 | 33 | versions.sort { |a, b| 34 | Dr::PkgVersion.new(a) <=> Dr::PkgVersion.new(b) 35 | }.reverse 36 | end 37 | 38 | def build_exists?(version) 39 | File.directory? "#{@repo.location}/packages/#{@name}/builds/#{version}" 40 | end 41 | 42 | def remove_build(version) 43 | raise "Build #{version.fg("blue")} not found" unless build_exists? version 44 | FileUtils.rm_rf "#{@repo.location}/packages/#{@name}/builds/#{version}" 45 | end 46 | 47 | def get_configuration 48 | {} 49 | end 50 | 51 | def set_configuration(config) 52 | raise "This package isn't configurable" 53 | end 54 | 55 | def <=>(o) 56 | self.name <=> o.name 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/dr/pkgversion.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2018 Kano Computing Ltd. 2 | # License: http://www.gnu.org/licenses/gpl-2.0.txt GNU GPL v2 3 | 4 | module Dr 5 | class PkgVersion 6 | attr_accessor :epoch, :upstream, :debian, :date, :build 7 | 8 | def initialize(version_string) 9 | @epoch = 0 # integer 10 | @upstream = "" # string 11 | @debian = "" # string 12 | @date = 0 # integer 13 | @build = 0 # integer 14 | 15 | # Make sure the version is string 16 | version_string = version_string.to_s 17 | 18 | v = version_string.split ":" 19 | if v.length > 1 20 | @epoch = v[0].to_i 21 | version_string = v[1..-1].join ":" 22 | end 23 | 24 | v = version_string.split "-" 25 | if v.length == 1 26 | @upstream = version_string 27 | else 28 | @upstream = v[0...-1].join "-" 29 | 30 | # Check whether the is a build tag in the debian version 31 | dv = v[-1].split "." 32 | if dv.length == 1 33 | @debian = v[-1] 34 | else 35 | @debian = dv[0] 36 | 37 | build_tag = dv[1..-1].join "." 38 | 39 | if build_tag =~ /^[0-9]{8}/ 40 | @date = dv[1][0..7].to_i 41 | 42 | match = dv[1].match(/build([0-9]+)$/) 43 | if match 44 | @build = match.captures[0].to_i 45 | end 46 | else 47 | # The part behind the '.' isn't a valid build tag, 48 | # append the string back to debian version. 49 | @debian << '.' << build_tag 50 | end 51 | end 52 | end 53 | end 54 | 55 | def increment! 56 | if @date == today 57 | @build += 1 58 | else 59 | @date = today 60 | end 61 | 62 | self 63 | end 64 | 65 | def <(o) 66 | compare(o) < 0 67 | end 68 | 69 | def >(o) 70 | compare(o) > 0 71 | end 72 | 73 | def <=(o) 74 | compare(o) <= 0 75 | end 76 | 77 | def >=(o) 78 | compare(o) >= 0 79 | end 80 | 81 | def ==(o) 82 | compare(o) == 0 83 | end 84 | 85 | def <=>(o) 86 | compare(o) 87 | end 88 | 89 | def to_s(omit_epoch=false) 90 | v = @upstream.clone 91 | 92 | if @epoch > 0 and not omit_epoch 93 | v = "#{@epoch}:#{v}" 94 | end 95 | 96 | if @debian.length > 0 97 | v << "-#{@debian}" 98 | end 99 | 100 | if @date > 0 101 | v << ".#{@date}" 102 | 103 | if @build > 0 104 | v << "build#{@build}" 105 | end 106 | end 107 | 108 | v 109 | end 110 | 111 | def source 112 | v = upstream.to_s 113 | v = "#{epoch}:#{v}" if @epoch > 0 114 | v << "-#{debian}" if @debian.length > 0 115 | v 116 | end 117 | 118 | def add_build_tag 119 | @date = today 120 | end 121 | 122 | private 123 | def today 124 | Time.now.strftime("%Y%m%d").to_i 125 | end 126 | 127 | def compare(o) 128 | return @epoch <=> o.epoch if @epoch != o.epoch 129 | 130 | result = debian_version_string_compare @upstream, o.upstream 131 | return result if result != 0 132 | 133 | result = debian_version_string_compare @debian, o.debian 134 | return result if result != 0 135 | 136 | result = @date <=> o.date 137 | return result if result != 0 138 | 139 | @build <=> o.build 140 | end 141 | 142 | # Compare two version strings (either upstream or debian versions) 143 | # in the Debian way 144 | def debian_version_string_compare(str1, str2) 145 | phase = :string 146 | loop do 147 | return 0 if str1.length == 0 && str2.length == 0 148 | return -1 if str1.length == 0 149 | return 1 if str2.length == 0 150 | 151 | if phase == :digit 152 | part1 = str1.match(/^[0-9]*/)[0] 153 | str1 = str1.sub(/^[0-9]*/, "") 154 | 155 | part2 = str2.match(/^[0-9]*/)[0] 156 | str2 = str2.sub(/^[0-9]*/, "") 157 | 158 | result = part1.to_i <=> part2.to_i 159 | return result if result != 0 160 | phase = :string 161 | else 162 | part1 = str1.match(/^[^0-9]*/)[0] 163 | str1 = str1.sub(/^[^0-9]*/, "") 164 | 165 | part2 = str2.match(/^[^0-9]*/)[0] 166 | str2 = str2.sub(/^[^0-9]*/, "") 167 | 168 | result = debian_string_compare part1, part2 169 | return result if result != 0 170 | phase = :digit 171 | end 172 | end 173 | end 174 | 175 | # Compare two strings without any digits in the Debian way 176 | def debian_string_compare(str1, str2) 177 | return 0 if str1.length == 0 && str2.length == 0 178 | return -1 if str1.length == 0 179 | return 1 if str2.length == 0 180 | 181 | c1 = str1[0] 182 | c2 = str2[0] 183 | 184 | # Both characters are letters and are not equal 185 | # -> compare them and return the result 186 | return c1 <=> c2 if is_letter(c1) && is_letter(c2) && c1 != c2 187 | 188 | # Both characters are non-letters and are not equal 189 | # We need to sort out ~ being less than everything 190 | if !is_letter(c1) && !is_letter(c2) 191 | if c1 != c2 192 | return -1 if c1 == '~' 193 | return 1 if c2 == '~' 194 | return c1 <=> c2 195 | end 196 | end 197 | 198 | # If one is a letter and one isn't, non-letter is always smaller 199 | return -1 if !is_letter(c1) && is_letter(c2) 200 | return 1 if is_letter(c1) && !is_letter(c2) 201 | 202 | # The characters are equal, compare the rest of the string 203 | return debian_string_compare str1[1..-1], str2[1..-1] 204 | end 205 | 206 | def is_letter(str) 207 | (str =~ /[a-z]/i) != nil 208 | end 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /lib/dr/repo.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2018 Kano Computing Ltd. 2 | # License: http://www.gnu.org/licenses/gpl-2.0.txt GNU General Public License v2 3 | 4 | require "dr/gitpackage" 5 | require "dr/debpackage" 6 | 7 | require "dr/shellcmd" 8 | require "dr/logger" 9 | require "dr/gnupg" 10 | require "dr/buildroot" 11 | require "dr/utils" 12 | require "dr/pkgversion" 13 | require "dr/threadpool" 14 | 15 | require "fileutils" 16 | require "yaml" 17 | 18 | module Dr 19 | class AlreadyExists < StandardError; end 20 | 21 | class Repo 22 | include Logger 23 | 24 | # TODO: Migrate all reprepro calls to use mutex 25 | @@reprepro_mutex = Mutex.new 26 | 27 | attr_reader :location 28 | 29 | def get_archive_path 30 | "#{@location}/archive" 31 | end 32 | 33 | def initialize(loc) 34 | @location = File.expand_path loc 35 | 36 | @packages_dir = "#{@location}/packages" 37 | end 38 | 39 | def setup(conf) 40 | log :info, "Creating the archive directory" 41 | begin 42 | FileUtils.mkdir_p location 43 | rescue Exception => e 44 | log :err, "Unable to create a directory at '#{@location.fg("blue")}'" 45 | raise e 46 | end 47 | 48 | FileUtils.mkdir_p "#{@location}/archive" 49 | 50 | gpg = GnuPG.new "#{@location}/gnupg-keyring" 51 | key = gpg.generate_key conf[:gpg_name], conf[:gpg_mail], conf[:gpg_pass] 52 | gpg.export_pub key, "#{@location}/archive/repo.gpg.key" 53 | 54 | log :info, "Writing the configuration file" 55 | FileUtils.mkdir_p "#{@location}/archive/conf" 56 | File.open "#{@location}/archive/conf/distributions", "w" do |f| 57 | conf[:suites].each_with_index do |s, i| 58 | f.puts "Suite: #{s}" 59 | 60 | if conf[:codenames][i].length > 0 61 | f.puts "Codename: #{conf[:codenames][i]}" 62 | end 63 | 64 | if conf[:name].length > 0 65 | f.puts "Origin: #{conf[:name]} - #{s}" 66 | f.puts "Label: #{conf[:name]} - #{s}" 67 | end 68 | 69 | if conf[:desc].length > 0 70 | f.puts "Description: #{conf[:desc]}" 71 | end 72 | 73 | f.puts "Architectures: #{conf[:arches].join " "}" 74 | f.puts "Components: #{conf[:components].join " "}" 75 | 76 | f.puts "SignWith: #{key}" 77 | f.puts "" 78 | end 79 | end 80 | 81 | FileUtils.mkdir_p @packages_dir 82 | FileUtils.mkdir_p "#{@location}/buildroots" 83 | 84 | metadata = { 85 | "default_build_environment" => conf[:build_environment].to_s 86 | } 87 | File.open("#{@location}/metadata", "w" ) do |out| 88 | out.write metadata.to_yaml 89 | end 90 | end 91 | 92 | def get_configuration 93 | meta_file = "#{@location}/metadata" 94 | if File.exist? meta_file 95 | Utils::symbolise_keys YAML.load_file(meta_file) 96 | else 97 | {} 98 | end 99 | end 100 | 101 | def set_configuration(new_metadata) 102 | # TODO: Some validation needed 103 | File.open("#{@location}/metadata", "w" ) do |out| 104 | out.write Utils::stringify_symbols(new_metadata).to_yaml 105 | end 106 | end 107 | 108 | def list_packages(suite=nil) 109 | pkgs = [] 110 | 111 | if suite 112 | mutex = Mutex.new 113 | 114 | thread_pool(Dir.entries @packages_dir) do |pkg_name| 115 | unless pkg_name =~ /^\./ 116 | versions = get_subpackage_versions pkg_name 117 | unless versions[codename_to_suite suite].empty? 118 | pkg = get_package pkg_name 119 | mutex.synchronize do 120 | pkgs.push pkg 121 | end 122 | end 123 | end 124 | end 125 | else 126 | Dir.foreach @packages_dir do |pkg_name| 127 | pkgs.push get_package pkg_name unless pkg_name =~ /^\./ 128 | end 129 | end 130 | 131 | pkgs.sort 132 | end 133 | 134 | def has_arch(arch, build_env=:default) 135 | if build_env == :default 136 | build_env = get_configuration[:default_build_environment].to_sym 137 | end 138 | arch == 'all' or Dr.config.build_environments[build_env][:arches].include? arch 139 | end 140 | 141 | def buildroot(arch, build_env=:default) 142 | if build_env == :default 143 | build_env = get_configuration[:default_build_environment].to_sym 144 | end 145 | 146 | cache_dir = "#{@location}/buildroots/" 147 | BuildRoot.new build_env, arch, cache_dir 148 | end 149 | 150 | def get_package(name) 151 | unless File.exist? "#{@packages_dir}/#{name}" 152 | raise "Package #{name.style "pkg-name"} doesn't exist in the repo" 153 | end 154 | 155 | if File.exist? "#{@packages_dir}/#{name}/source" 156 | GitPackage.new name, self 157 | else 158 | DebPackage.new name, self 159 | end 160 | end 161 | 162 | def get_suites 163 | suites = nil 164 | File.open "#{@location}/archive/conf/distributions", "r" do |f| 165 | suites = f.read.split "\n\n" 166 | end 167 | 168 | suites.map do |s| 169 | suite = nil 170 | codename = nil 171 | s.each_line do |l| 172 | m = l.match /^Suite: (.+)/ 173 | suite = m.captures[0].chomp if m 174 | 175 | m = l.match /^Codename: (.+)/ 176 | codename = m.captures[0].chomp if m 177 | end 178 | [suite, codename] 179 | end 180 | end 181 | 182 | def get_architectures 183 | arches = [] 184 | File.open "#{@location}/archive/conf/distributions", "r" do |f| 185 | f.each_line do |l| 186 | m = l.match /^Architectures: (.+)/ 187 | arches += m.captures[0].chomp.split(" ") if m 188 | end 189 | end 190 | 191 | arches.uniq 192 | end 193 | 194 | def query_for_deb_version(suite, pkg_name) 195 | reprepro_cmd = "reprepro --basedir #{location}/archive " + 196 | "--list-format '${version}' list #{suite} " + 197 | "#{pkg_name} 2>/dev/null" 198 | reprepro = ShellCmd.new reprepro_cmd, :tag => "reprepro" 199 | v = reprepro.out.chomp 200 | v = nil unless v.length > 0 201 | v 202 | end 203 | 204 | def suite_has_package?(suite, pkg_name) 205 | pkg_versions = get_subpackage_versions(pkg_name)[codename_to_suite(suite)] 206 | 207 | pkg_versions.length > 0 208 | end 209 | 210 | def suite_has_higher_pkg_version?(suite, pkg, version) 211 | used_versions = get_subpackage_versions(pkg.name)[codename_to_suite(suite)] 212 | 213 | has_higher_version = false 214 | used_versions.each do |subpkg_name, subpkg_version| 215 | if subpkg_version.to_s >= version.to_s 216 | has_higher_version = true 217 | end 218 | end 219 | has_higher_version 220 | end 221 | 222 | def get_subpackage_versions(pkg_name) 223 | pkg = get_package pkg_name 224 | suites = get_suites 225 | 226 | versions = {} 227 | suites.each do |suite, codename| 228 | versions[suite] = {} 229 | reprepro_cmd = "reprepro --basedir #{location}/archive " + 230 | "--list-format '${package} ${version}\n' " + 231 | "listfilter #{suite} 'Source (== #{pkg_name}) | " + 232 | "Package (== #{pkg_name})' " + 233 | "2>/dev/null" 234 | 235 | reprepro = nil 236 | @@reprepro_mutex.synchronize do 237 | reprepro = ShellCmd.new reprepro_cmd, :tag => "reprepro" 238 | end 239 | reprepro.out.chomp.each_line do |line| 240 | subpkg, version = line.split(" ").map(&:chomp) 241 | versions[suite][subpkg] = version 242 | end 243 | end 244 | versions 245 | end 246 | 247 | def push(pkg_name, version, suite, force=false) 248 | pkg = get_package pkg_name 249 | 250 | if version 251 | unless pkg.build_exists? version 252 | raise "Build version '#{version}' not found" 253 | end 254 | else 255 | if pkg.history.length == 0 256 | log :err, "No built packages available for #{pkg_name}" 257 | log :err, "Please, run a build first and the push." 258 | raise "Push failed" 259 | end 260 | version = pkg.history[0] 261 | end 262 | 263 | if suite 264 | cmp = get_suites.map { |n, cn| suite == n || suite == cn } 265 | suite_exists = cmp.inject(false) { |r, o| r || o } 266 | raise "Suite '#{suite}' doesn't exist." unless suite_exists 267 | else 268 | # FIXME: This should be configurable 269 | suite = "testing" 270 | end 271 | 272 | debs = Dir["#{@location}/packages/#{pkg.name}/builds/#{version}/*"] 273 | names = debs.map { |deb| File.basename(deb).split("_")[0] } 274 | 275 | used_versions = get_subpackage_versions(pkg.name)[codename_to_suite(suite)] 276 | 277 | is_of_higher_version = true 278 | names.each do |name| 279 | if used_versions.has_key?(name) && 280 | PkgVersion.new(version) <= PkgVersion.new(used_versions[name]) 281 | is_of_higher_version = false 282 | end 283 | end 284 | 285 | unless is_of_higher_version 286 | log :warn, "The #{suite} suite already contains " + 287 | "#{pkg.name.style "pkg-name"} version " + 288 | "#{version.to_s.style "version"}" 289 | if force 290 | reprepro = "reprepro -b #{@location}/archive " + 291 | "--gnupghome #{location}/gnupg-keyring/ removesrc " + 292 | "#{suite} #{pkg.name}" 293 | ShellCmd.new reprepro, :tag => "reprepro", :show_out => false 294 | else 295 | log :warn, "The same package of a higher version is already in the " + 296 | "#{suite} suite." 297 | 298 | raise AlreadyExists.new "Push failed" 299 | end 300 | end 301 | 302 | log :info, "Pushing #{pkg_name.style "pkg-name"} version " + 303 | "#{version.to_s.style "version"} to #{suite}" 304 | reprepro = "reprepro -b #{@location}/archive " + 305 | "--gnupghome #{location}/gnupg-keyring/ includedeb " + 306 | "#{suite} #{debs.join " "}" 307 | ShellCmd.new reprepro, :tag => "reprepro", :show_out => true 308 | end 309 | 310 | def unpush(pkg_name, suite) 311 | pkg = get_package pkg_name 312 | 313 | cmp = get_suites.map { |n, cn| suite == n || suite == cn } 314 | suite_exists = cmp.inject(false) { |r, o| r || o } 315 | unless suite_exists 316 | log :err, "Suite '#{suite}' doesn't exist." 317 | raise "Unpush failed" 318 | end 319 | 320 | log :info, "Removing #{pkg_name.style "pkg-name"} from #{suite}" 321 | reprepro = "reprepro -b #{@location}/archive " + 322 | "--gnupghome #{location}/gnupg-keyring/ removesrc " + 323 | "#{suite} #{pkg.name}" 324 | ShellCmd.new reprepro, :tag => "reprepro", :show_out => true 325 | end 326 | 327 | def remove(pkg_name, force=false) 328 | pkg = get_package pkg_name 329 | 330 | if is_used? pkg_name 331 | log :warn, "The #{pkg_name.style "pkg-name"} package is still used" 332 | raise "Operation canceled, add -f to remove anyway" unless force 333 | 334 | log :info, "Will be force-removed anyway" 335 | versions = get_subpackage_versions(pkg_name) 336 | get_suites.each do |suite, codename| 337 | unpush pkg_name, suite unless versions[suite].empty? 338 | end 339 | end 340 | 341 | log :info, "Removing #{pkg_name.style "pkg-name"} from the repository" 342 | FileUtils.rm_rf "#{location}/packages/#{pkg_name}" 343 | end 344 | 345 | def remove_build(pkg_name, version, force=false) 346 | pkg = get_package pkg_name 347 | 348 | if is_used?(pkg_name, version) 349 | if force 350 | log :info, "Force-removing #{version.style "version"} version of " + 351 | "#{pkg_name.style "pkg-name"}" 352 | versions_by_suite = get_subpackage_versions pkg_name 353 | versions_by_suite.each do |suite, versions| 354 | unpush pkg_name, suite if versions.has_value? version 355 | end 356 | else 357 | log :warn, "This build of #{pkg_name.style "pkg-name"} is " + 358 | "still being used, add -f to force-remove" 359 | return 360 | end 361 | else 362 | log :info, "Removing the #{version.style "version"} version of " + 363 | "#{pkg_name.style "pkg-name"}" 364 | end 365 | 366 | pkg.remove_build version 367 | end 368 | 369 | def get_build(pkg_name, version=nil) 370 | pkg = get_package pkg_name 371 | 372 | hist = pkg.history 373 | raise "The package hasn't been built yet." unless hist.length > 0 374 | version = hist[0] unless version 375 | 376 | unless pkg.build_exists? version 377 | raise "Build #{version.style "version"} doesn't exist" 378 | end 379 | 380 | Dir["#{@location}/packages/#{pkg.name}/builds/#{version}/*"] 381 | end 382 | 383 | def get_build_metadata(pkg_name, version) 384 | pkg = get_package pkg_name 385 | raise "Build #{version} for package #{pkg_name} doesn't exist" unless pkg.build_exists? version 386 | 387 | md_file = "#{@location}/packages/#{pkg.name}/builds/#{version}/.metadata" 388 | if File.exist? md_file 389 | YAML.load_file md_file 390 | else 391 | {} 392 | end 393 | end 394 | 395 | def sign_deb(deb) 396 | keyring = "#{@location}/gnupg-keyring" 397 | gpg = GnuPG.new keyring 398 | key_id = gpg.get_key_id get_key 399 | 400 | cmd = "dpkg-sig -k '#{key_id}' -s builder -g '--homedir #{keyring}' #{deb}" 401 | ShellCmd.new cmd, :tag => "dpkg-sig", :show_out => true 402 | end 403 | 404 | def codename_to_suite(codename_or_suite) 405 | get_suites.each do |suite, codename| 406 | return suite if codename_or_suite == suite || codename_or_suite == codename 407 | end 408 | 409 | nil 410 | end 411 | 412 | def suite_to_codename(codename_or_suite) 413 | get_suites.each do |suite, codename| 414 | return codename if codename_or_suite == suite || codename_or_suite == codename 415 | end 416 | 417 | nil 418 | end 419 | 420 | def is_used?(pkg_name, version=nil) 421 | versions_by_suite = get_subpackage_versions pkg_name 422 | versions_by_suite.inject(false) do |rslt, hash_pair| 423 | suite, versions = hash_pair 424 | if version == nil 425 | rslt || !versions.empty? 426 | else 427 | rslt || versions.has_value?(version) 428 | end 429 | end 430 | end 431 | 432 | private 433 | def get_key 434 | File.open "#{@location}/archive/conf/distributions", "r" do |f| 435 | f.each_line do |line| 436 | m = line.match /^SignWith: (.+)/ 437 | return m.captures[0] if m 438 | end 439 | end 440 | end 441 | end 442 | end 443 | -------------------------------------------------------------------------------- /lib/dr/server.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Kano Computing Ltd. 2 | # License: http://www.gnu.org/licenses/gpl-2.0.txt GNU GPL v2 3 | 4 | require "rack" 5 | 6 | module Dr 7 | class Server 8 | def initialize(port, root_route, address, archive_path) 9 | @port = port 10 | @host = address 11 | @dir_server = Rack::Builder.new do 12 | map root_route do 13 | run Rack::Directory.new(archive_path) 14 | end 15 | end 16 | end 17 | 18 | def start 19 | Rack::Handler::Thin.run(@dir_server, :Port => @port, :Host => @host) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/dr/shellcmd.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2018 Kano Computing Ltd. 2 | # License: http://www.gnu.org/licenses/gpl-2.0.txt GNU GPL v2 3 | 4 | require "open3" 5 | require "tco" 6 | 7 | require "dr/logger" 8 | 9 | module Dr 10 | class ShellCmd 11 | attr_reader :status, :out, :err 12 | 13 | include Logger 14 | 15 | def initialize(cmd, opts={}) 16 | @out = "" 17 | 18 | @show_out = false 19 | @raise_on_error = true 20 | @tag = "shell" 21 | @expect = 0 22 | 23 | opts.each do |k, v| 24 | self.instance_variable_set "@#{k}", v 25 | end 26 | 27 | @cmd = cmd 28 | @status = nil 29 | 30 | run 31 | end 32 | 33 | private 34 | def run 35 | Open3.popen2e(@cmd) do |stdin, stdouterr, wait_thr| 36 | pid = wait_thr.pid 37 | 38 | begin 39 | stdouterr.fsync = true 40 | stdouterr.sync = true 41 | rescue 42 | a = 1 # FIXME 43 | end 44 | 45 | while line = stdouterr.gets 46 | @out += line 47 | if @show_out 48 | line = tag(@tag.dup, line) if @tag 49 | log(:info, line) 50 | end 51 | end 52 | 53 | wait_thr.join 54 | @status = wait_thr.value 55 | end 56 | 57 | if (@expect.is_a?(Array) && !@expect.include?(@status.exitstatus)) || 58 | (@expect.is_a?(Integer) && @status.exitstatus != @expect) 59 | out_lines = @out.split "\n" 60 | if out_lines.length > 10 61 | out_lines = out_lines[-10..-1] 62 | end 63 | 64 | out_lines.each do |l| 65 | l = tag(@tag, l.fg("red")) if @tag 66 | log(:err, l.chomp) 67 | end 68 | raise "'#{@cmd}' failed!".fg("red") if @raise_on_error 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/dr/threadpool.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Kano Computing Ltd. 2 | # License: http://www.gnu.org/licenses/gpl-2.0.txt GNU GPLv2 3 | 4 | 5 | require "thread" 6 | 7 | def thread_pool(items, worker_count=8) 8 | work_queue = Queue.new 9 | items.each { |item| work_queue.push item } 10 | 11 | threads = (0 .. worker_count).map do 12 | Thread.new do 13 | begin 14 | # Passing `true` causes the queue to raise an exception if it is empty 15 | # rather than block, waiting for something to add to it. 16 | while item = work_queue.pop(true) 17 | yield item 18 | end 19 | rescue ThreadError 20 | # The work_queue is empty, we are done 21 | end 22 | end 23 | end 24 | 25 | threads.map(&:join) 26 | end 27 | -------------------------------------------------------------------------------- /lib/dr/utils.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Kano Computing Ltd. 2 | # License: http://www.gnu.org/licenses/gpl-2.0.txt GNU General Public License v2 3 | 4 | module Dr 5 | module Utils 6 | def self.symbolise_keys(hash) 7 | if hash.is_a? Hash 8 | hash.inject({}) do |new_hash, (key, value)| 9 | new_hash[key.to_sym] = symbolise_keys value 10 | new_hash 11 | end 12 | else 13 | hash 14 | end 15 | end 16 | 17 | def self.stringify_keys(hash) 18 | return hash unless hash.is_a? Hash 19 | 20 | hash.inject({}) do |new_hash, (key, value)| 21 | new_hash[key.to_s] = stringify_keys value 22 | new_hash 23 | end 24 | end 25 | 26 | def self.stringify_symbols(var) 27 | case 28 | when var.is_a?(Hash) 29 | var.inject({}) do |new_hash, (key, value)| 30 | new_hash[key.to_s] = stringify_keys value 31 | new_hash 32 | end 33 | when var.is_a?(Array) 34 | var.map {|e| stringify_symbols e} 35 | when var.is_a?(Symbol) 36 | var.to_s 37 | else 38 | var 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/dr/version.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2019 Kano Computing Ltd. 2 | # License: http://www.gnu.org/licenses/gpl-2.0.txt GNU GPL v2 3 | 4 | module Dr 5 | VERSION = "1.3.8" 6 | end 7 | -------------------------------------------------------------------------------- /spec/pkgversion_spec.rb: -------------------------------------------------------------------------------- 1 | # Tests for the PkgVersion class 2 | # 3 | # Copyright (C) 2015 Kano Computing Ltd. 4 | # License: http://www.gnu.org/licenses/gpl-2.0.txt GNU GPL v2 5 | 6 | require 'dr/pkgversion' 7 | 8 | describe Dr do 9 | describe 'PkgVersion' do 10 | describe "compare based on epoch" do 11 | it "works when smaller" do 12 | one = Dr::PkgVersion.new('1:1.5-1') 13 | two = Dr::PkgVersion.new('2:1.5-1') 14 | 15 | expect(one < two).to be true 16 | end 17 | 18 | it "works when equal" do 19 | one = Dr::PkgVersion.new('1:2.7-2') 20 | two = Dr::PkgVersion.new('1:2.7-2') 21 | 22 | expect(one == two).to be true 23 | end 24 | 25 | it "works when smaller" do 26 | one = Dr::PkgVersion.new('2:3.6-1') 27 | two = Dr::PkgVersion.new('1:3.5') 28 | 29 | expect(one > two).to be true 30 | end 31 | end 32 | 33 | describe "compare based on upstram version" do 34 | it "works when epoch is equal" do 35 | one = Dr::PkgVersion.new('1:1.5-1') 36 | two = Dr::PkgVersion.new('1:1.6-1') 37 | 38 | expect(one < two).to be true 39 | end 40 | 41 | it "works when smaller" do 42 | one = Dr::PkgVersion.new('1-1') 43 | two = Dr::PkgVersion.new('2') 44 | 45 | expect(one < two).to be true 46 | end 47 | 48 | it "works when smaller with string inbetween" do 49 | one = Dr::PkgVersion.new('1.5') 50 | two = Dr::PkgVersion.new('1.16-5') 51 | 52 | expect(one < two).to be true 53 | end 54 | 55 | it "works when equal" do 56 | one = Dr::PkgVersion.new('1.5-5') 57 | two = Dr::PkgVersion.new('1.5-5') 58 | 59 | expect(one == two).to be true 60 | end 61 | 62 | it "equal with no debian version" do 63 | one = Dr::PkgVersion.new('15') 64 | two = Dr::PkgVersion.new('15') 65 | 66 | expect(one == two).to be true 67 | end 68 | 69 | it "works when bigger" do 70 | one = Dr::PkgVersion.new('6.5') 71 | two = Dr::PkgVersion.new('1.16-5') 72 | 73 | expect(one > two).to be true 74 | end 75 | end 76 | 77 | describe "compare based on debian version" do 78 | it "smaller comparison" do 79 | one = Dr::PkgVersion.new('1.5-1') 80 | two = Dr::PkgVersion.new('1.5-2') 81 | 82 | expect(one < two).to be true 83 | end 84 | 85 | it "equal comparison" do 86 | one = Dr::PkgVersion.new('1.5-2') 87 | two = Dr::PkgVersion.new('1.5-2') 88 | 89 | expect(one == two).to be true 90 | end 91 | 92 | it "bigger comparison" do 93 | one = Dr::PkgVersion.new('1.5-11') 94 | two = Dr::PkgVersion.new('1.5-9') 95 | 96 | expect(one > two).to be true 97 | end 98 | 99 | it "substring comparison" do 100 | one = Dr::PkgVersion.new('1.5-111') 101 | two = Dr::PkgVersion.new('1.5-11') 102 | 103 | expect(one > two).to be true 104 | end 105 | end 106 | 107 | describe "build tags" do 108 | it "build date parsed correctly" do 109 | v = Dr::PkgVersion.new('1.5-1.20150323') 110 | expect(v.date).to eq 20150323 111 | end 112 | 113 | it "build number parsed correctly" do 114 | v = Dr::PkgVersion.new('1.5-1.20150323build9') 115 | expect(v.build).to eq 9 116 | end 117 | 118 | it "debian version parsed correctly with build tag" do 119 | v = Dr::PkgVersion.new('1.5-7.20150323build9') 120 | expect(v.debian).to eq "7" 121 | end 122 | 123 | it "debian version includes malformed build tag" do 124 | v = Dr::PkgVersion.new('1.5-7.a20150323build9') 125 | expect(v.debian).to eq "7.a20150323build9" 126 | end 127 | end 128 | 129 | describe "comparison with build tags" do 130 | it "smaller date" do 131 | one = Dr::PkgVersion.new('1.5-7.20150320') 132 | two = Dr::PkgVersion.new('1.5-7.20150323') 133 | 134 | expect(one < two).to be true 135 | end 136 | 137 | it "bigger date" do 138 | one = Dr::PkgVersion.new('1.5-7.20150328') 139 | two = Dr::PkgVersion.new('1.5-7.20150323') 140 | 141 | expect(one > two).to be true 142 | end 143 | 144 | it "equal dates" do 145 | one = Dr::PkgVersion.new('1.5-7.20150328') 146 | two = Dr::PkgVersion.new('1.5-7.20150328') 147 | 148 | expect(one == two).to be true 149 | end 150 | 151 | it "equal dates with build numbers (smaller)" do 152 | one = Dr::PkgVersion.new('1.5-7.20150328build1') 153 | two = Dr::PkgVersion.new('1.5-7.20150328build5') 154 | 155 | expect(one < two).to be true 156 | end 157 | 158 | it "equal dates with build numbers (equal)" do 159 | one = Dr::PkgVersion.new('1.5-7.20150328build15') 160 | two = Dr::PkgVersion.new('1.5-7.20150328build15') 161 | 162 | expect(one == two).to be true 163 | end 164 | 165 | it "equal dates with build numbers (bigger)" do 166 | one = Dr::PkgVersion.new('1.5-7.20150328build15') 167 | two = Dr::PkgVersion.new('1.5-7.20150328build5') 168 | 169 | expect(one > two).to be true 170 | end 171 | 172 | it "build number substrings" do 173 | one = Dr::PkgVersion.new('1.5-7.20150328build11') 174 | two = Dr::PkgVersion.new('1.5-7.20150328build111') 175 | 176 | expect(one < two).to be true 177 | end 178 | end 179 | end 180 | end 181 | --------------------------------------------------------------------------------