autoproj 2.8.8 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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']