autoproj 2.8.8 → 2.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,137 @@
1
+ require 'tempfile'
2
+ require 'json'
3
+
4
+ module Autoproj
5
+ # Manager for OS repository provided by package sets
6
+ class OSRepositoryResolver
7
+ # All the information contained in all the OSrepos files
8
+ attr_reader :all_definitions
9
+
10
+ # The operating system
11
+ attr_accessor :operating_system
12
+
13
+ def self.load(file)
14
+ raise ArgumentError, "no such file or directory: #{file}" unless File.file?(file)
15
+
16
+ error_t = if defined? Psych::SyntaxError
17
+ [ArgumentError, Psych::SyntaxError]
18
+ else
19
+ ArgumentError
20
+ end
21
+
22
+ result = new
23
+ file = File.expand_path(file)
24
+ begin
25
+ data = YAML.safe_load(File.read(file)) || {}
26
+ verify_definitions(data)
27
+ rescue *error_t => e
28
+ raise ConfigError.new, "error in #{file}: #{e.message}", e.backtrace
29
+ end
30
+ result.merge(new(data, file))
31
+ result
32
+ end
33
+
34
+ def initialize(defs = [], file = nil, operating_system: nil)
35
+ @operating_system = operating_system
36
+ @all_definitions = Set.new
37
+ if file
38
+ defs.each do |def_|
39
+ all_definitions << [[file], def_]
40
+ end
41
+ else
42
+ defs.each do |def_|
43
+ all_definitions << [[], def_]
44
+ end
45
+ end
46
+ end
47
+
48
+ def merge(info)
49
+ @all_definitions += info.all_definitions
50
+ end
51
+
52
+ def definitions
53
+ all_definitions.map(&:last).uniq
54
+ end
55
+
56
+ def all_entries
57
+ definitions.map do |distribution|
58
+ distribution.values.map do |release|
59
+ release.map(&:values)
60
+ end
61
+ end.flatten.uniq
62
+ end
63
+
64
+ def entry_matches?(entry, identifiers)
65
+ !(entry.keys.first.split(',').map(&:strip) & identifiers).empty?
66
+ end
67
+
68
+ def resolved_entries
69
+ os_name, os_version = operating_system
70
+ os_version << 'default' unless os_version.include?('default')
71
+
72
+ distribution_filtered = definitions.select do |entry|
73
+ entry_matches?(entry, os_name)
74
+ end.map(&:values).flatten
75
+
76
+ release_filtered = distribution_filtered.select { |entry| entry_matches?(entry, os_version) }
77
+ release_filtered.map(&:values).flatten.uniq
78
+ end
79
+
80
+ # OS repos definitions must follow the format:
81
+ #
82
+ # - distribution:
83
+ # - release:
84
+ # - key1: value1
85
+ # key2: value2
86
+ #
87
+ # The key, value pairs are OS dependent, and will be verified/parsed by the
88
+ # corresponding 'repository manager'. Thus, for a debian-like distribution
89
+ # one would have something like:
90
+ #
91
+ # - ubuntu:
92
+ # - xenial:
93
+ # - type: repo
94
+ # repo: 'deb http://br.archive.ubuntu.com/ubuntu/ xenial main restricted'
95
+ def self.verify_definitions(array, path = [])
96
+ verify_type(array, Array, path)
97
+ array.each do |entry|
98
+ verify_type(entry, Hash, path)
99
+ verify_os(entry, (path + [entry]))
100
+ end
101
+ end
102
+
103
+ def self.verify_os(hash, path = [])
104
+ verify_type(hash, Hash, path)
105
+ hash.each do |key, value|
106
+ verify_type(key, String, path)
107
+ verify_release(value, (path + [value]))
108
+ end
109
+ end
110
+
111
+ def self.verify_release(array, path = [])
112
+ verify_type(array, Array, path)
113
+ array.each do |releases|
114
+ verify_type(releases, Hash, path)
115
+
116
+ releases.values.each do |entries|
117
+ verify_type(entries, Array, path)
118
+ verify_entries(entries, path + entries)
119
+ end
120
+ end
121
+ end
122
+
123
+ def self.verify_entries(array, path = [])
124
+ verify_type(array, Array, path)
125
+ array.each do |entry|
126
+ verify_type(entry, Hash, path)
127
+ end
128
+ end
129
+
130
+ def self.verify_type(obj, type, path = [])
131
+ return if obj.is_a?(type)
132
+
133
+ raise ArgumentError, "invalid osrepos definition in #{path.join('/')}: "\
134
+ "expected a #{type}, found a #{obj.class}"
135
+ end
136
+ end
137
+ end
@@ -1,3 +1,5 @@
1
+ require 'autoproj/package_managers/debian_version'
2
+
1
3
  module Autoproj
2
4
  module PackageManagers
3
5
  # Package manager interface for systems that use APT and dpkg for
@@ -8,15 +10,36 @@ class AptDpkgManager < ShellScriptManager
8
10
  def initialize(ws, status_file = "/var/lib/dpkg/status")
9
11
  @status_file = status_file
10
12
  @installed_packages = nil
13
+ @installed_versions = nil
11
14
  super(ws, true,
12
15
  %w{apt-get install},
13
16
  %w{DEBIAN_FRONTEND=noninteractive apt-get install -y})
14
17
  end
15
18
 
16
- def self.parse_package_status(installed_packages, paragraph)
19
+ def configure_manager
20
+ super
21
+ ws.config.declare 'apt_dpkg_update', 'boolean',
22
+ default: 'yes',
23
+ doc: ['Would you like autoproj to keep apt packages up-to-date?']
24
+ keep_uptodate?
25
+ end
26
+
27
+ def keep_uptodate?
28
+ ws.config.get('apt_dpkg_update')
29
+ end
30
+
31
+ def keep_uptodate=(flag)
32
+ ws.config.set('apt_dpkg_update', flag, true)
33
+ end
34
+
35
+ def self.parse_package_status(installed_packages, installed_versions, paragraph)
17
36
  if paragraph =~ /^Status: install ok installed$/
18
37
  if paragraph =~ /^Package: (.*)$/
19
- installed_packages << $1
38
+ package_name = $1
39
+ installed_packages << package_name
40
+ if paragraph =~ /^Version: (.*)$/
41
+ installed_versions[package_name] = DebianVersion.new($1)
42
+ end
20
43
  end
21
44
  if paragraph =~ /^Provides: (.*)$/
22
45
  installed_packages.merge($1.split(',').map(&:strip))
@@ -26,6 +49,7 @@ def self.parse_package_status(installed_packages, paragraph)
26
49
 
27
50
  def self.parse_dpkg_status(status_file)
28
51
  installed_packages = Set.new
52
+ installed_versions = {}
29
53
  dpkg_status = File.read(status_file)
30
54
  dpkg_status << "\n"
31
55
 
@@ -36,17 +60,53 @@ def self.parse_dpkg_status(status_file)
36
60
 
37
61
  while paragraph_end = dpkg_status.scan_until(/Package: /)
38
62
  paragraph = "Package: #{paragraph_end[0..-10]}"
39
- parse_package_status(installed_packages, paragraph)
63
+ parse_package_status(installed_packages, installed_versions, paragraph)
64
+ end
65
+ parse_package_status(installed_packages, installed_versions, "Package: #{dpkg_status.rest}")
66
+ [installed_packages, installed_versions]
67
+ end
68
+
69
+ def self.parse_apt_cache_paragraph(paragraph)
70
+ version = '0'
71
+ if paragraph =~ /^Package: (.*)$/
72
+ package_name = $1
73
+ if paragraph =~ /^Version: (.*)$/
74
+ version = $1
75
+ end
76
+ end
77
+ [package_name, version]
78
+ end
79
+
80
+ def self.parse_packages_versions(packages)
81
+ packages_versions = {}
82
+ apt_cache_show = `apt-cache show --no-all-versions #{packages.join(' ')}`
83
+ apt_cache_show = StringScanner.new(apt_cache_show)
84
+ if !apt_cache_show.scan(/Package: /)
85
+ return packages_versions
86
+ end
87
+
88
+ while paragraph_end = apt_cache_show.scan_until(/Package: /)
89
+ paragraph = "Package: #{paragraph_end[0..-10]}"
90
+ package_name, version = parse_apt_cache_paragraph(paragraph)
91
+ packages_versions[package_name] = DebianVersion.new(version)
40
92
  end
41
- parse_package_status(installed_packages, "Package: #{dpkg_status.rest}")
42
- installed_packages
93
+ package_name, version = parse_apt_cache_paragraph("Package: #{apt_cache_show.rest}")
94
+ packages_versions[package_name] = DebianVersion.new(version)
95
+ packages_versions
96
+ end
97
+
98
+ def updated?(package, available_version)
99
+ # Consider up-to-date if the package is provided by another package (purely virtual)
100
+ # Ideally, we should check the version of the package that provides it
101
+ return true unless available_version && @installed_versions[package]
102
+
103
+ (available_version <= @installed_versions[package])
43
104
  end
44
105
 
45
106
  # On a dpkg-enabled system, checks if the provided package is installed
46
107
  # and returns true if it is the case
47
108
  def installed?(package_name, filter_uptodate_packages: false, install_only: false)
48
- @installed_packages ||= AptDpkgManager.parse_dpkg_status(status_file)
49
-
109
+ @installed_packages, @installed_versions = self.class.parse_dpkg_status(status_file) unless @installed_packages && @installed_versions
50
110
  if package_name =~ /^(\w[a-z0-9+-.]+)/
51
111
  @installed_packages.include?($1)
52
112
  else
@@ -54,11 +114,12 @@ def installed?(package_name, filter_uptodate_packages: false, install_only: fals
54
114
  false
55
115
  end
56
116
  end
57
-
117
+
58
118
  def install(packages, filter_uptodate_packages: false, install_only: false)
119
+ packages_versions = self.class.parse_packages_versions(packages)
59
120
  if filter_uptodate_packages || install_only
60
121
  packages = packages.find_all do |package_name|
61
- !installed?(package_name)
122
+ !installed?(package_name) || (keep_uptodate? && !updated?(package_name, packages_versions[package_name]))
62
123
  end
63
124
  end
64
125
 
@@ -71,4 +132,3 @@ def install(packages, filter_uptodate_packages: false, install_only: false)
71
132
  end
72
133
  end
73
134
  end
74
-
@@ -213,7 +213,7 @@ def self.run_bundler(ws, *commandline, gem_home: nil, gemfile: nil)
213
213
  'GEM_PATH' => nil,
214
214
  'BUNDLE_GEMFILE' => gemfile,
215
215
  'RUBYOPT' => nil,
216
- 'RUBYLIB' => nil
216
+ 'RUBYLIB' => rubylib_for_bundler
217
217
  ]
218
218
  ws.run 'autoproj', 'osdeps',
219
219
  Autobuild.tool('bundle'), *commandline,
@@ -388,6 +388,11 @@ def discover_rubylib
388
388
  end
389
389
  end
390
390
 
391
+ def self.rubylib_for_bundler
392
+ rx = Regexp.new("/gems/#{Regexp.quote("bundler-#{Bundler::VERSION}")}/")
393
+ $LOAD_PATH.grep(rx).join(File::PATH_SEPARATOR)
394
+ end
395
+
391
396
  def discover_bundle_rubylib(silent_errors: false)
392
397
  require 'bundler'
393
398
  gemfile = File.join(ws.prefix_dir, 'gems', 'Gemfile')
@@ -398,7 +403,9 @@ def discover_bundle_rubylib(silent_errors: false)
398
403
  env = ws.env.resolved_env
399
404
  Tempfile.open 'autoproj-rubylib' do |io|
400
405
  result = Bundler.clean_system(
401
- Hash['GEM_HOME' => env['GEM_HOME'], 'GEM_PATH' => env['GEM_PATH'], 'BUNDLE_GEMFILE' => gemfile, 'RUBYOPT' => nil, 'RUBYLIB' => nil],
406
+ Hash['GEM_HOME' => env['GEM_HOME'], 'GEM_PATH' => env['GEM_PATH'],
407
+ 'BUNDLE_GEMFILE' => gemfile, 'RUBYOPT' => nil,
408
+ 'RUBYLIB' => self.class.rubylib_for_bundler],
402
409
  Autobuild.tool('ruby'), '-rbundler/setup', '-e', 'puts $LOAD_PATH',
403
410
  out: io, **silent_redirect)
404
411
 
@@ -0,0 +1,123 @@
1
+ module Autoproj
2
+ module PackageManagers
3
+ class DebianVersion
4
+ attr_reader :version
5
+ attr_reader :epoch
6
+ attr_reader :upstream_version
7
+ attr_reader :debian_revision
8
+
9
+ include Comparable
10
+
11
+ def initialize(version)
12
+ @version = version
13
+ parse_version
14
+ end
15
+
16
+ def split
17
+ [epoch, upstream_version, debian_revision]
18
+ end
19
+
20
+ def <=>(b)
21
+ (0..2).inject(0) do |result, i|
22
+ return result unless result == 0
23
+ normalize(compare_fragments(self.split[i], b.split[i]))
24
+ end
25
+ end
26
+
27
+ def self.compare(a, b)
28
+ new(a)<=>new(b)
29
+ end
30
+
31
+ private
32
+
33
+ def normalize(value)
34
+ return -1 if value < 0
35
+ return 1 if value > 0
36
+ return 0 if value == 0
37
+ end
38
+
39
+ # Reference: https://www.debian.org/doc/debian-policy/ch-controlfields.html#version
40
+ def parse_version
41
+ @epoch = '0'
42
+ @debian_revision = '0'
43
+
44
+ @upstream_version = @version.split(':')
45
+ if @upstream_version.size > 1
46
+ @epoch = @upstream_version.first
47
+ @upstream_version = @upstream_version[1..-1].join(':')
48
+ else
49
+ @upstream_version = @upstream_version.first
50
+ end
51
+
52
+ @upstream_version = @upstream_version.split('-')
53
+ if @upstream_version.size > 1
54
+ @debian_revision = @upstream_version.last
55
+ @upstream_version = @upstream_version[0..-2].join('-')
56
+ else
57
+ @upstream_version = @upstream_version.first
58
+ end
59
+ end
60
+
61
+ def alpha?(look_ahead)
62
+ look_ahead =~ /[[:alpha:]]/
63
+ end
64
+
65
+ def digit?(look_ahead)
66
+ look_ahead =~ /[[:digit:]]/
67
+ end
68
+
69
+ def order(c)
70
+ if digit?(c)
71
+ return 0
72
+ elsif alpha?(c)
73
+ return c.ord
74
+ elsif c == '~'
75
+ return -1
76
+ elsif c
77
+ return c.ord + 256
78
+ else
79
+ return 0
80
+ end
81
+ end
82
+
83
+ # Ported from https://github.com/Debian/apt/blob/master/apt-pkg/deb/debversion.cc
84
+ def compare_fragments(a, b)
85
+ i = 0
86
+ j = 0
87
+ while i != a.size && j != b.size
88
+ first_diff = 0
89
+ while i != a.size && j != b.size && (!digit?(a[i]) || !digit?(b[j]))
90
+ vc = order(a[i])
91
+ rc = order(b[j])
92
+ return vc-rc if vc != rc
93
+ i += 1
94
+ j += 1
95
+ end
96
+
97
+ i += 1 while a[i] == '0'
98
+ j += 1 while b[j] == '0'
99
+ while digit?(a[i]) && digit?(b[j])
100
+ first_diff = a[i].ord - b[j].ord if first_diff == 0
101
+ i += 1
102
+ j += 1
103
+ end
104
+
105
+ return 1 if digit?(a[i])
106
+ return -1 if digit?(b[j])
107
+ return first_diff if first_diff != 0
108
+ end
109
+
110
+ return 0 if i == a.size && j == b.size
111
+
112
+ if i == a.size
113
+ return 1 if b[j] == '~'
114
+ return -1
115
+ end
116
+ if j == b.size
117
+ return -1 if a[i] == '~'
118
+ return 1
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -41,6 +41,11 @@ def strict?
41
41
  false
42
42
  end
43
43
 
44
+ # If this package manager depends on OS packages, they should be added here
45
+ def os_dependencies
46
+ []
47
+ end
48
+
44
49
  # Create a package manager
45
50
  #
46
51
  # @param [Workspace] ws the underlying workspace
@@ -56,6 +61,10 @@ def initialize(ws)
56
61
  # This is e.g. needed for python pip or rubygems
57
62
  def initialize_environment
58
63
  end
64
+
65
+ # Perform additional configuration required for the package manager
66
+ def configure_manager
67
+ end
59
68
  end
60
69
  end
61
70
  end
@@ -20,6 +20,10 @@ def initialize(ws)
20
20
  @installed_pips = Set.new
21
21
  end
22
22
 
23
+ def os_dependencies
24
+ super + ['pip']
25
+ end
26
+
23
27
  def guess_pip_program
24
28
  if Autobuild.programs['pip']
25
29
  return Autobuild.programs['pip']