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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +112 -0
- data/Rakefile +8 -0
- data/autoproj-sync.gemspec +29 -0
- data/lib/autoproj-sync.rb +39 -0
- data/lib/autoproj/cli/main_sync.rb +189 -0
- data/lib/autoproj/sync.rb +7 -0
- data/lib/autoproj/sync/config.rb +84 -0
- data/lib/autoproj/sync/remote.rb +399 -0
- data/lib/autoproj/sync/version.rb +5 -0
- metadata +113 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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).
|
data/Rakefile
ADDED
@@ -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,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
|
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: []
|