autoproj-sync 0.1.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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: af756c3be787939a54e4d4f17cb1b8091737000678c8850e2e20741a1db0a5f2
4
+ data.tar.gz: 2640991f20d18f9972ddeeeab0aa77832987515b13a38829310983b157703c89
5
+ SHA512:
6
+ metadata.gz: 0020ac4629195c4f9a802735cd1c3a5ec6f5e3fa3167d15d1efa1602a258422c300b8903c5228cc31574b7928c14b8329db56386d824355668848200f376f1fc
7
+ data.tar.gz: d0900fb01449b29bde7678b286932e72aa849f27b548e42ab930e2933d8cde636dde9893a612534ee52820875368ce346dce445bf9233c961c02d53b645ddc64
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ Gemfile.lock
10
+ /vendor/
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.16.2
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in autoproj-sync.gemspec
6
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Sylvain Joyeux
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,112 @@
1
+ # Autoproj::Sync
2
+
3
+ This Autoproj plugin provides a way to keep remote location(s) synchronized
4
+ with a local workspace, as part of the build process. This allows for to keep
5
+ a local development workflow, but run things remotely in a very flexible way.
6
+
7
+ It is not *that* magical. It is based on the assumption that:
8
+ - the local and remote hosts have equivalent environments. In practice, it means
9
+ that binaries built with the local machine are compatible with the environment
10
+ on the remote machine (same shared libraries or using static libraries, ...)
11
+ - the remote environment is synchronized at the exact same absolute path than
12
+ the local one
13
+
14
+ ## Installation
15
+
16
+ Run
17
+
18
+ ```
19
+ autoproj plugin install autoproj-sync
20
+ ```
21
+
22
+ Autoproj Sync works only on workspaces that use separate prefixes. If you did
23
+ not enable prefixes when bootstrapping your workspace, the easiest is to
24
+ re-bootstrap a new clean one, passing the `--separate-prefixes` option.
25
+
26
+ ## Usage
27
+
28
+ ### Preparing the remote target
29
+
30
+ To function, Autoproj Sync needs the remote target to match the local target, that is:
31
+
32
+ - allow to run binaries built locally (meaning same or compatible shared libraries)
33
+ - use the same full paths
34
+ - have a compatible ruby binary at the same path than the one used locally. If you
35
+ for instance use rbenv, you will need to install the same Ruby version through rbenv.
36
+ - if you are using a git version of Autoproj, or plugins that are not available through
37
+ RubyGems, you will have to install them manually at the same path than on the local
38
+ machine
39
+
40
+ Moreover the remote target should be accessible via SSH public key authentication.
41
+
42
+ ### Automated Synchronisation
43
+
44
+ Once a target is added and enabled (see below on how to do this), Autoproj will sync
45
+ it automatically after an update or build operation. This works well with
46
+
47
+ ### Sync and the VSCode/Rock integration
48
+
49
+ The vscode-rock extension can use sync to debug remote programs.
50
+
51
+ This currently only supports C++ programs. When creating your launch entry, simply
52
+ add the following line to it:
53
+
54
+ ~~~json
55
+ "miDebuggerServerAddress": "rock:<remote_name>:<remote_port>"
56
+ ~~~
57
+
58
+ where `<remote_name>` is the name of the Sync remote and `<remote_port>` the
59
+ port that should be used by `gdbserver`. Run, and that's it.
60
+
61
+ ### CLI Usage
62
+
63
+ Add and enable a synchronisation target with
64
+
65
+ ```
66
+ autoproj sync add NAME URL
67
+ ```
68
+
69
+ This will trigger a synchronization.
70
+
71
+ Targets can be listed with
72
+
73
+ ```
74
+ autoproj sync list
75
+ ```
76
+
77
+ And removed with
78
+
79
+ ```
80
+ autoproj sync remove NAME
81
+ ```
82
+
83
+ Synchronisation can be temporarily disabled with
84
+
85
+ ```
86
+ autoproj sync disable
87
+ ```
88
+
89
+ And reenabled with
90
+
91
+ ```
92
+ autoproj sync enable
93
+ ```
94
+
95
+ Re-enabling a target will force a synchronisation to occur. Once a target is
96
+ enabled, synchronisation happens during the build, whenever a package has
97
+ been built and installed.
98
+
99
+ The `enable` and `disable` subcommands both accept target names, which allows
100
+ to selectively enable and disable.
101
+
102
+ ```
103
+ autoproj sync enable
104
+ ```
105
+
106
+ ## Contributing
107
+
108
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rock-core/autoproj-sync.
109
+
110
+ ## License
111
+
112
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
@@ -0,0 +1,29 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "autoproj/sync/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "autoproj-sync"
8
+ spec.version = Autoproj::Sync::VERSION
9
+ spec.authors = ["Sylvain Joyeux"]
10
+ spec.email = ["sylvain.joyeux@13robotics.com"]
11
+
12
+ spec.summary = %q{Synchronizes the build byproducts of an Autoproj workspace to remote target(s)}
13
+ spec.homepage = "https://rock-core.github.io/rock-and-syskit"
14
+ spec.license = "MIT"
15
+
16
+ # Specify which files should be added to the gem when it is released.
17
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
18
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
19
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
+ end
21
+ spec.bindir = "exe"
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_dependency "autoproj", "~> 2.4"
26
+ spec.add_dependency "net-sftp"
27
+ spec.add_development_dependency "bundler", "~> 1.16"
28
+ spec.add_development_dependency "minitest", "~> 5.0"
29
+ end
@@ -0,0 +1,39 @@
1
+ require 'autoproj/cli/main_sync'
2
+
3
+ class Autoproj::CLI::Main
4
+ desc 'sync', 'synchronize a workspace with remote location(s)'
5
+ subcommand 'sync', Autoproj::CLI::MainSync
6
+
7
+ register_post_command_hook(:build) do |ws, args|
8
+ source_packages = args[:source_packages]
9
+ source_packages = source_packages.map do |package_name|
10
+ ws.manifest.find_package_definition(package_name)
11
+ end
12
+
13
+ config = Autoproj::Sync::Config.new(ws)
14
+ config.each_enabled_remote.each do |remote|
15
+ remote.start do |sftp|
16
+ remote.update(sftp, ws, source_packages)
17
+ end
18
+ end
19
+ end
20
+ register_post_command_hook(:update) do |ws, args|
21
+ source_packages, osdep_packages = args.
22
+ values_at(:source_packages, :osdep_packages)
23
+ source_packages = source_packages.map do |package_name|
24
+ ws.manifest.find_package_definition(package_name)
25
+ end
26
+
27
+ config = Autoproj::Sync::Config.new(ws)
28
+ config.each_enabled_remote.each do |remote|
29
+ remote.start do |sftp|
30
+ unless source_packages.empty?
31
+ remote.update(sftp, ws, source_packages)
32
+ end
33
+ unless osdep_packages.empty?
34
+ remote.osdeps(sftp, ws, osdep_packages)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,189 @@
1
+ require 'autoproj/sync'
2
+
3
+ module Autoproj
4
+ module CLI
5
+ # The 'jenkins' subcommand for autoproj
6
+ class MainSync < Thor
7
+ namespace 'sync'
8
+
9
+ no_commands do
10
+ def ws
11
+ unless @ws
12
+ @ws = Autoproj::Workspace.default
13
+ @ws.load_config
14
+ unless @ws.config.separate_prefixes?
15
+ raise RuntimeError, "autoproj-sync only works on workspaces "\
16
+ "that have separate prefixes enabled"
17
+ end
18
+ end
19
+ @ws
20
+ end
21
+
22
+ def config
23
+ unless @config
24
+ @config = Sync::Config.new(ws)
25
+ end
26
+ @config
27
+ end
28
+
29
+ def ws_load
30
+ ws.setup
31
+ ws.load_package_sets
32
+ ws.setup_all_package_directories
33
+ ws.finalize_package_setup
34
+
35
+ source_packages, osdep_packages, _resolved_selection =
36
+ ws.load_packages(ws.manifest.default_packages(false),
37
+ recursive: true,
38
+ non_imported_packages: :ignore,
39
+ auto_exclude: false)
40
+ ws.finalize_setup
41
+ source_packages = source_packages.map do |name|
42
+ ws.manifest.find_package_definition(name)
43
+ end
44
+ [source_packages, osdep_packages]
45
+ end
46
+
47
+ def resolve_selected_remotes(*names)
48
+ if names.empty?
49
+ config.each_enabled_remote
50
+ else
51
+ names.map { |n| config.remote_by_name(n) }
52
+ end
53
+ end
54
+
55
+ end
56
+
57
+ desc 'add NAME URL', "add a new remote target"
58
+ def add(name, uri)
59
+ if remote = config.find_remote_by_name(name)
60
+ STDERR.puts "There is already a target called #{name} pointing to "\
61
+ "#{remote.uri}"
62
+ exit 1
63
+ end
64
+
65
+ remote = Sync::Remote.from_uri(URI.parse(uri), name: name)
66
+ config.add_remote(remote)
67
+ packages, = Autoproj.silent { ws_load }
68
+ remote.start do |sftp|
69
+ remote.update(sftp, ws, packages)
70
+ end
71
+ end
72
+
73
+ desc 'remove NAME', "remove a remote target"
74
+ def remove(name)
75
+ config.delete_remote(name)
76
+ end
77
+
78
+ desc 'list', "lists registered targets"
79
+ def list
80
+ config.each_remote do |remote|
81
+ enabled = remote.enabled? ? 'enabled' : 'disabled'
82
+ puts "#{remote.name}: #{remote.uri} (#{enabled})"
83
+ end
84
+ end
85
+
86
+ desc 'status [NAME]', "accesses a target (or all enabled targets) and "\
87
+ "display outdated packages"
88
+ def status(*names)
89
+ remotes = resolve_selected_remotes(*names)
90
+ packages, = Autoproj.silent { ws_load }
91
+
92
+ remotes.each do |r|
93
+ outdated_packages =
94
+ r.start do |sftp|
95
+ r.each_outdated_package(sftp, @ws, packages).to_a
96
+ end
97
+ puts "#{outdated_packages.size} outdated packages"
98
+ outdated_packages.each do |pkg|
99
+ puts " #{pkg.name}"
100
+ end
101
+ end
102
+ end
103
+
104
+ desc 'update NAME', "trigger an update for a remote"
105
+ def update(*names)
106
+ remotes = resolve_selected_remotes(*names)
107
+ packages, = Autoproj.silent { ws_load }
108
+
109
+ remotes.each do |r|
110
+ r.start do |sftp|
111
+ r.update(sftp, ws, packages)
112
+ end
113
+ end
114
+ end
115
+
116
+ desc 'enable NAME', "enables a previously disabled target, or all targets"
117
+ def enable(*names)
118
+ names = ws.config.get('sync', Hash.new).keys if names.empty?
119
+ packages = nil
120
+ names.each do |name|
121
+ config.update_remote_config(name) do |remote_config|
122
+ unless remote_config['enabled']
123
+ remote = config.remote_by_name(name)
124
+ packages, = Autoproj.silent { ws_load } unless packages
125
+ remote.start do |sftp|
126
+ remote.update(sftp, ws, packages)
127
+ end
128
+ remote_config['enabled'] = true
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ desc 'disable [NAME]', "disables a previously enabled target, or all targets"
135
+ def disable(*names)
136
+ names = ws.config.get('sync', Hash.new).keys if names.empty?
137
+ names.each do |name|
138
+ config.update_remote_config(name) do |config|
139
+ config['enabled'] = false
140
+ end
141
+ end
142
+ end
143
+
144
+ desc 'install-osdeps MANAGER_TYPE <PACKAGES...>',
145
+ 'install osdeps coming from the local machine',
146
+ hide: true
147
+ def install_osdeps(manager_type, *packages)
148
+ Autobuild.silent { ws_load }
149
+ installer = ws.os_package_installer
150
+
151
+ installer.setup_package_managers
152
+ manager = installer.package_managers.fetch(manager_type)
153
+ installer.install_manager_packages(manager, packages)
154
+ end
155
+
156
+ desc 'osdeps [NAME]', 'install the osdeps on the remote'
157
+ def osdeps(*names)
158
+ _, osdep_packages = Autobuild.silent { ws_load }
159
+ remotes = resolve_selected_remotes(*names)
160
+ remotes.each do |r|
161
+ r.start do |sftp|
162
+ r.osdeps(sftp, ws, osdep_packages)
163
+ end
164
+ end
165
+ end
166
+
167
+ desc 'exec NAME COMMAND', 'execute a command on a remote workspace'
168
+ option :interactive, doc: 'execute the command in interactive mode',
169
+ type: :boolean, default: false
170
+ option :chdir, doc: 'working directory for the command',
171
+ type: :string, default: nil
172
+ def exec(remote_name, *command)
173
+ remote = config.remote_by_name(remote_name)
174
+ status = remote.start do |sftp|
175
+ remote.remote_autoproj(sftp, ws.root_dir, "exec", *command,
176
+ interactive: options[:interactive],
177
+ chdir: options[:chdir] || ws.root_dir)
178
+ end
179
+ unless options[:interactive]
180
+ if status[:exit_signal]
181
+ exit 255
182
+ else
183
+ exit status[:exit_code]
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,7 @@
1
+ require 'autoproj'
2
+ require 'uri'
3
+ require 'pathname'
4
+ require 'net/sftp'
5
+ require 'autoproj/sync/version'
6
+ require 'autoproj/sync/config'
7
+ require 'autoproj/sync/remote'
@@ -0,0 +1,84 @@
1
+ module Autoproj
2
+ module Sync
3
+ class Config
4
+ def initialize(ws)
5
+ @ws = ws
6
+ end
7
+
8
+ def each_remote
9
+ return enum_for(__method__) unless block_given?
10
+
11
+ targets = @ws.config.get('sync', Hash.new)
12
+ targets.each do |name, config|
13
+ yield Remote.from_uri(URI.parse(config['uri']),
14
+ name: name, enabled: config['enabled'])
15
+ end
16
+ end
17
+
18
+ def each_enabled_remote
19
+ return enum_for(__method__) unless block_given?
20
+
21
+ each_remote do |remote|
22
+ yield(remote) if remote.enabled?
23
+ end
24
+ end
25
+
26
+ def remote_by_name(name)
27
+ unless remote = find_remote_by_name(name)
28
+ raise ArgumentError, "no remote named '#{name}', "\
29
+ "existing remotes: #{each_remote.map(&:name).sort.join(", ")}"
30
+ end
31
+ remote
32
+ end
33
+
34
+ def find_remote_by_name(name)
35
+ targets = @ws.config.get('sync', Hash.new)
36
+ if config = targets[name]
37
+ remote_from_config(name, config)
38
+ end
39
+ end
40
+
41
+ private def remote_from_config(name, config)
42
+ Remote.from_uri(URI.parse(config['uri']),
43
+ name: name, enabled: config['enabled'])
44
+ end
45
+
46
+ private def remote_to_config(remote)
47
+ [remote.name, Hash[
48
+ 'uri' => remote.uri.to_s,
49
+ 'enabled' => remote.enabled?
50
+ ]]
51
+ end
52
+
53
+ def add_remote(remote)
54
+ name, config = remote_to_config(remote)
55
+ targets = @ws.config.get('sync', Hash.new)
56
+ targets[name] = config
57
+ @ws.config.set('sync', targets)
58
+ @ws.save_config
59
+ end
60
+
61
+ def delete_remote(name)
62
+ targets = @ws.config.get('sync', Hash.new)
63
+ targets.delete(name)
64
+ @ws.config.set('sync', targets)
65
+ @ws.save_config
66
+ end
67
+
68
+ def update_remote_config(name)
69
+ targets = @ws.config.get('sync', Hash.new)
70
+ unless (config = targets[name])
71
+ raise ArgumentError, "There is no target called #{name}"
72
+ end
73
+
74
+ new_config = config.dup
75
+ yield(new_config)
76
+ if new_config != config
77
+ targets[name] = new_config
78
+ @ws.config.set('sync', targets)
79
+ @ws.save_config
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,399 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autoproj
4
+ module Sync
5
+ class Remote
6
+ class FailedRemoteCommand < RuntimeError; end
7
+
8
+ attr_reader :uri
9
+ attr_reader :name
10
+
11
+ def self.from_uri(uri, name: uri, enabled: true)
12
+ if uri.scheme != "ssh"
13
+ raise ArgumentError, "unsupported protocol #{uri.scheme}"
14
+ end
15
+ Remote.new(uri, name: name, enabled: enabled)
16
+ end
17
+
18
+ def initialize(uri, name: uri, enabled: true)
19
+ @uri = uri
20
+ @enabled = enabled
21
+ @name = name
22
+ end
23
+
24
+ def enabled?
25
+ @enabled
26
+ end
27
+
28
+ def remote_path
29
+ @uri.path
30
+ end
31
+
32
+ def start
33
+ result = nil
34
+ Net::SFTP.start(@uri.host, @uri.user, password: @uri.password) do |sftp|
35
+ result = yield(sftp)
36
+ end
37
+ result
38
+ end
39
+
40
+ # Enumerate the packages that are outdated on the remote
41
+ #
42
+ # @yieldparam [Net::SFTP::Session] sftp the opened SFTP session, that can
43
+ # be used to do further operations on the remote
44
+ # @yieldparam [Autoproj::PackageDescription] an outdated package
45
+ def each_outdated_package(sftp, ws, packages)
46
+ return enum_for(__method__, sftp, ws, packages) unless block_given?
47
+
48
+ stat = packages.map do |package|
49
+ autobuild = package.autobuild
50
+ installstamp = autobuild.installstamp
51
+ next unless File.exist?(installstamp)
52
+
53
+ local_stat = File.stat(installstamp)
54
+ remote_path = File.join(uri.path, installstamp)
55
+ begin
56
+ remote_stat = sftp.file.open(remote_path) do |f|
57
+ f.stat
58
+ end
59
+ [package, local_stat, remote_stat, remote_path]
60
+ rescue Net::SFTP::StatusException => e
61
+ if e.code != Net::SFTP::Constants::StatusCodes::FX_NO_SUCH_FILE
62
+ raise
63
+ end
64
+ package
65
+ end
66
+ end
67
+
68
+ stat.compact.map do |package, local_stat, remote_stat, remote_path|
69
+ yield(package) if !local_stat ||
70
+ changed_stat?(local_stat, remote_stat)
71
+ end.compact
72
+ end
73
+
74
+ private def changed_stat?(local, remote)
75
+ return true if local.size != remote.size
76
+
77
+ local_sec = local.mtime.tv_sec
78
+ local_usec = local.mtime.tv_usec
79
+ if remote.mtime != local_sec
80
+ true
81
+ elsif !remote.respond_to?(:remote_nseconds)
82
+ false
83
+ else
84
+ (remote.mtime_nseconds / 1000) != local_usec
85
+ end
86
+ end
87
+
88
+ private def remote_mkdir_p(sftp, local_path)
89
+ remote_path = File.join(@uri.path, local_path)
90
+ ops = []
91
+ while remote_path != '/'
92
+ ops << [remote_path, sftp.stat(remote_path)]
93
+ remote_path = File.dirname(remote_path)
94
+ end
95
+ missing = ops.take_while do |_, op|
96
+ op.wait
97
+ !op.response.ok?
98
+ end
99
+ mkdirs = missing.reverse.map do |path, _|
100
+ sftp.mkdir(path)
101
+ end
102
+ mkdirs.each(&:wait)
103
+ end
104
+
105
+ def autoproj_annex_files(ws)
106
+ user_files = %w[env.sh].
107
+ map do |file|
108
+ File.join(ws.root_dir, file)
109
+ end
110
+ autoproj_files = %w[env.yml installation-manifest].
111
+ map do |file|
112
+ File.join(ws.root_dir, '.autoproj', file)
113
+ end
114
+ bundler_files = %w[gems/Gemfile gems/Gemfile.lock].
115
+ map do |file|
116
+ File.join(ws.prefix_dir, file)
117
+ end
118
+ [*user_files, *ws.env.source_before, *ws.env.source_after,
119
+ *autoproj_files, *bundler_files]
120
+ end
121
+
122
+ def rsync_target
123
+ if @uri.user && @uri.password
124
+ "#{@uri.user}:#{@uri.password}@#{@uri.host}"
125
+ elsif @uri.user
126
+ "#{@uri.user}@#{@uri.host}"
127
+ else
128
+ @uri.host
129
+ end
130
+ end
131
+
132
+ def rsync_dir(sftp, local_dir)
133
+ remote_dir = remote_path(local_dir)
134
+ ["rsync", "-a", "--delete-after", "#{local_dir}/",
135
+ "#{rsync_target}:#{remote_dir}/"]
136
+ end
137
+
138
+ def rsync_file(sftp, local_file)
139
+ remote_file = remote_path(local_file)
140
+ ["rsync", "-a", local_file,
141
+ "#{rsync_target}:#{remote_file}"]
142
+ end
143
+
144
+ def osdeps(sftp, ws, osdep_packages)
145
+ installer = ws.os_package_installer
146
+
147
+ installer.setup_package_managers
148
+ all = ws.all_os_packages
149
+ partitioned_packages = installer.
150
+ resolve_and_partition_osdep_packages(osdep_packages, all)
151
+
152
+ os_packages = partitioned_packages.delete(installer.os_package_manager)
153
+ if os_packages
154
+ partitioned_packages = [[installer.os_package_manager, os_packages]].
155
+ concat(partitioned_packages.to_a)
156
+ end
157
+
158
+ partitioned_packages = partitioned_packages.map do |manager, packages|
159
+ manager_name, _ = installer.package_managers.
160
+ find { |key, obj| manager == obj }
161
+ [manager_name, packages]
162
+ end
163
+
164
+ partitioned_packages.each do |manager_name, packages|
165
+ install_osdep_packages(sftp, ws, manager_name, packages)
166
+ end
167
+ end
168
+
169
+ def install_osdep_packages(sftp, ws, manager_name, packages)
170
+ Autobuild.progress_start "sync-#{name}-osdeps-#{manager_name}",
171
+ "sync: handling #{packages.size} osdeps #{manager_name} packages "\
172
+ "on #{name}",
173
+ done_message: "sync: handled #{packages.size} "\
174
+ "osdeps #{manager_name} packages on #{name}" do
175
+
176
+ result = remote_autoproj(sftp, ws.root_dir,
177
+ "sync", "install-osdeps",
178
+ manager_name, *packages)
179
+ if result[:exit_code] != 0
180
+ raise RuntimeError,
181
+ "remote autoproj command failed\n"\
182
+ "autoproj exited with status "\
183
+ "#{result[:exit_code]}\n#{result}"
184
+ end
185
+ end
186
+ end
187
+
188
+ def create_package_directories(sftp, pkg)
189
+ Autobuild.progress_start pkg, "sync: preparing #{pkg.name}@#{name}",
190
+ done_message: "sync: prepared #{pkg.name}@#{name}" do
191
+
192
+ remote_mkdir_p(sftp, pkg.autobuild.prefix)
193
+ remote_mkdir_p(sftp, File.dirname(pkg.autobuild.installstamp))
194
+ end
195
+ end
196
+
197
+ def rsync_package(sftp, pkg)
198
+ Autobuild.progress_start pkg, "sync: updating #{pkg.name}@#{name}",
199
+ done_message: "sync: updated #{pkg.name}@#{name}" do
200
+ ops = [rsync_dir(sftp, pkg.autobuild.prefix),
201
+ rsync_file(sftp, pkg.autobuild.installstamp)]
202
+ ops.each do |op|
203
+ if !system(*op)
204
+ raise "update of #{pkg.name} failed"
205
+ end
206
+ end
207
+ end
208
+ end
209
+
210
+ def remote_path(local_path)
211
+ File.join(@uri.path, local_path)
212
+ end
213
+
214
+ def remote_file_exist?(sftp, path)
215
+ sftp.stat!(remote_path(path))
216
+ true
217
+ rescue Net::SFTP::StatusException => e
218
+ if e.code == Net::SFTP::Constants::StatusCodes::FX_NO_SUCH_FILE
219
+ false
220
+ else
221
+ raise
222
+ end
223
+ end
224
+
225
+ def remote_file_get(sftp, local_path)
226
+ sftp.download!(remote_path(local_path))
227
+ end
228
+
229
+ def remote_file_put(sftp, local_path, content)
230
+ sftp.upload!(StringIO.new(content), remote_path(local_path))
231
+ end
232
+
233
+ def remote_file_transfer(sftp, local_path, target: remote_path(local_path))
234
+ sftp.upload!(local_path, target)
235
+ end
236
+
237
+ def remote_autoproj(sftp, root_dir, *command, chdir: nil, interactive: false)
238
+ remote_exec(sftp,
239
+ remote_path(File.join(root_dir, ".autoproj/bin/autoproj")),
240
+ *command, chdir: chdir, interactive: interactive)
241
+ end
242
+
243
+ def remote_exec(sftp, *command, chdir: nil, interactive: false)
244
+ if interactive
245
+ remote_interactive_exec(sftp, *command, chdir: chdir)
246
+ else
247
+ status = Hash.new
248
+ ios = Hash[:stdout => STDOUT, :stderr => STDOUT]
249
+ target_dir = @uri.path
250
+ target_dir = File.join(target_dir, chdir) if chdir
251
+ pid = nil
252
+ command = "cd '#{target_dir}' && "\
253
+ "echo \"AUTOPROJ_SYNC_PID=$$\" && "\
254
+ "exec '" + command.join("' '") + "'"
255
+ ch = sftp.session.exec(command, status: status) do |channel, stream, data|
256
+ if !pid && (m = /^AUTOPROJ_SYNC_PID=(\d+)/.match(data))
257
+ pid = Integer(m[1])
258
+ else
259
+ ios[stream].print(data)
260
+ ios[stream].flush
261
+ end
262
+ end
263
+
264
+ begin
265
+ ch.wait
266
+ status
267
+ rescue Interrupt
268
+ sftp.session.exec!("kill #{pid}") if pid
269
+ ch.wait
270
+ raise
271
+ end
272
+ end
273
+ end
274
+
275
+ def remote_interactive_exec(sftp, *command, chdir: nil)
276
+ channel = sftp.session.open_channel do |ch|
277
+ ch.on_data do |ch, data|
278
+ STDOUT.print data
279
+ STDOUT.flush
280
+ end
281
+ ch.on_extended_data do |ch, type, data|
282
+ STDERR.print data
283
+ end
284
+
285
+ ch.request_pty
286
+ ch.exec("cd '#{chdir}' && '" + command.join("' '") + "'")
287
+ end
288
+
289
+ ssh = sftp.session
290
+ while channel.active?
291
+ ssh.process(0.1)
292
+ begin
293
+ while true
294
+ data = STDIN.read_nonblock(4096)
295
+ channel.send_data(data)
296
+ end
297
+ rescue IO::WaitReadable
298
+ end
299
+ end
300
+ rescue EOFError
301
+ channel.close
302
+ end
303
+
304
+ def local_file_get(local_path)
305
+ File.read(local_path)
306
+ end
307
+
308
+ def info(message)
309
+ Autoproj.message " #{message}", force: true
310
+ end
311
+
312
+ def bootstrap_or_update_autoproj(sftp, ws)
313
+ gemfile_lock_path = File.join(ws.root_dir, ".autoproj/Gemfile.lock")
314
+ if remote_file_exist?(sftp, gemfile_lock_path)
315
+ remote_gemfile_lock = remote_file_get(sftp, gemfile_lock_path)
316
+ local_gemfile_lock = local_file_get(gemfile_lock_path)
317
+ if remote_gemfile_lock == local_gemfile_lock
318
+ info "remote Autoproj install up-to-date"
319
+ info "sync: Autoproj install up-to-date on #{name}"
320
+ return
321
+ end
322
+
323
+ info "sync: updating the Autoproj install on #{name}"
324
+
325
+ remote_file_put(sftp, gemfile_lock_path, local_gemfile_lock)
326
+ remote_file_transfer(
327
+ sftp, File.join(ws.root_dir, ".autoproj/Gemfile"))
328
+ remote_file_transfer(
329
+ sftp, File.join(ws.root_dir, ".autoproj/config.yml"))
330
+ result = remote_autoproj(
331
+ sftp, ws.root_dir, "update", "--autoproj")
332
+ unless result[:exit_code] == 0
333
+ raise FailedRemoteCommand, "failed to update Autoproj:\n"\
334
+ "autoproj update --autoproj finished with exit status "\
335
+ "#{result[:exit_code]}\n"\
336
+ "#{result}"
337
+ end
338
+ else
339
+ info "sync: installing Autoproj on #{name}"
340
+
341
+ autoproj_spec = Bundler.definition.specs.
342
+ find { |spec| spec.name == "autoproj" }
343
+ autoproj_dir = autoproj_spec.full_gem_path
344
+ install_script = File.join(autoproj_dir, "bin", "autoproj_install")
345
+ remote_mkdir_p(sftp, ws.root_dir)
346
+ sftp.upload!(install_script,
347
+ remote_path(File.join(ws.root_dir, "autoproj_install")))
348
+ remote_file_transfer(sftp, File.join(ws.root_dir, ".autoproj/config.yml"),
349
+ target: remote_path(File.join(ws.root_dir, 'bootstrap-config.yml')))
350
+ remote_file_transfer(sftp, File.join(ws.root_dir, ".autoproj/Gemfile"),
351
+ target: remote_path(File.join(ws.root_dir, 'bootstrap-Gemfile')))
352
+ result = sftp.session.exec!("cd '#{remote_path(ws.root_dir)}' && "\
353
+ "#{ws.config.ruby_executable} autoproj_install "\
354
+ "--gemfile bootstrap-Gemfile "\
355
+ "--seed-config bootstrap-config.yml")
356
+ if result.exitstatus != 0
357
+ raise RuntimeError, "failed to install autoproj: #{result}"
358
+ end
359
+ end
360
+ end
361
+
362
+ def update(sftp, ws, packages)
363
+ # First check if autoproj is bootstrapped on the target already
364
+ bootstrap_or_update_autoproj(sftp, ws)
365
+
366
+ packages = each_outdated_package(sftp, ws, packages).to_a
367
+
368
+ info "sync: #{packages.size} outdated packages on #{name}"
369
+
370
+ executor = Concurrent::FixedThreadPool.new(6)
371
+ futures = packages.map do |pkg|
372
+ create_package_directories(sftp, pkg)
373
+ Concurrent::Future.execute(executor: executor) do
374
+ rsync_package(sftp, pkg)
375
+ end
376
+ end
377
+
378
+ # Copy some autoproj installation-manifest files
379
+ Autobuild.progress_start "sync-#{name}-autoproj",
380
+ "sync: updating Autoproj configuration files on #{name}",
381
+ done_message: "sync: updated Autoproj configuration files on #{name}" do
382
+ autoproj_annex_files(ws).each do |file|
383
+ sftp.upload!(file, File.join(@uri.path, file))
384
+ end
385
+ end
386
+
387
+ futures.each_with_index do |f, i|
388
+ f.value!
389
+ end
390
+
391
+ ensure
392
+ if executor
393
+ executor.shutdown
394
+ executor.wait_for_termination
395
+ end
396
+ end
397
+ end
398
+ end
399
+ end
@@ -0,0 +1,5 @@
1
+ module Autoproj
2
+ module Sync
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: autoproj-sync
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sylvain Joyeux
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-11-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: autoproj
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: net-sftp
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.16'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.16'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5.0'
69
+ description:
70
+ email:
71
+ - sylvain.joyeux@13robotics.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".travis.yml"
78
+ - Gemfile
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - autoproj-sync.gemspec
83
+ - lib/autoproj-sync.rb
84
+ - lib/autoproj/cli/main_sync.rb
85
+ - lib/autoproj/sync.rb
86
+ - lib/autoproj/sync/config.rb
87
+ - lib/autoproj/sync/remote.rb
88
+ - lib/autoproj/sync/version.rb
89
+ homepage: https://rock-core.github.io/rock-and-syskit
90
+ licenses:
91
+ - MIT
92
+ metadata: {}
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubyforge_project:
109
+ rubygems_version: 2.7.6
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: Synchronizes the build byproducts of an Autoproj workspace to remote target(s)
113
+ test_files: []