3 # Remote Builds tool using rsync+ssh
5 __author__
= "Jérôme Carretero <cJ-waf@zougloub.eu>"
6 __copyright__
= "Jérôme Carretero, 2013"
12 This tool is an *experimental* tool (meaning, do not even try to pollute
13 the waf bug tracker with bugs in here, contact me directly) providing simple
16 It uses rsync and ssh to perform the remote builds.
17 It is intended for performing cross-compilation on platforms where
18 a cross-compiler is either unavailable (eg. MacOS, QNX) a specific product
19 does not exist (eg. Windows builds using Visual Studio) or simply not installed.
20 This tool sends the sources and the waf script to the remote host,
21 and commands the usual waf execution.
23 There are alternatives to using this tool, such as setting up shared folders,
24 logging on to remote machines, and building on the shared folders.
25 Electing one method or another depends on the size of the program.
31 1. Set your wscript file so it includes a list of variants,
34 from waflib import Utils
45 from waflib.extras import remote
48 # normal stuff from here on
49 opt.load('compiler_c')
54 # normal stuff from here on
55 conf.load('compiler_c')
60 # normal stuff from here on
61 bld(features='c cprogram', target='app', source='main.c')
64 2. Build the waf file, so it includes this tool, and put it in the current
69 ./waf-light --tools=remote
71 3. Set the host names to access the hosts:
75 export REMOTE_QNX=user@kiunix
77 4. Setup the ssh server and ssh keys
79 The ssh key should not be protected by a password, or it will prompt for it every time.
80 Create the key on the client:
84 ssh-keygen -t rsa -f foo.rsa
86 Then copy foo.rsa.pub to the remote machine (user@kiunix:/home/user/.ssh/authorized_keys),
87 and make sure the permissions are correct (chmod go-w ~ ~/.ssh ~/.ssh/authorized_keys)
89 A separate key for the build processes can be set in the environment variable WAF_SSH_KEY.
90 The tool will then use 'ssh-keyscan' to avoid prompting for remote hosts, so
91 be warned to use this feature on internal networks only (MITM).
95 export WAF_SSH_KEY=~/foo.rsa
101 waf configure_all build_all --remote
106 import getpass
, os
, re
, sys
107 from collections
import OrderedDict
108 from waflib
import Context
, Options
, Utils
, ConfigSet
110 from waflib
.Build
import BuildContext
, CleanContext
, InstallContext
, UninstallContext
111 from waflib
.Configure
import ConfigurationContext
115 if '--remote' in sys
.argv
:
117 sys
.argv
.remove('--remote')
119 class init(Context
.Context
):
121 Generates the *_all commands
126 for x
in list(Context
.g_module
.variants
):
129 for k
in Options
.commands
:
130 if k
.endswith('_all'):
131 name
= k
.replace('_all', '')
132 for x
in Context
.g_module
.variants
:
133 lst
.append('%s_%s' % (name
, x
))
136 del Options
.commands
[:]
137 Options
.commands
+= lst
139 def make_variant(self
, x
):
140 for y
in (BuildContext
, CleanContext
, InstallContext
, UninstallContext
):
141 name
= y
.__name
__.replace('Context','').lower()
146 class tmp(ConfigurationContext
):
147 cmd
= 'configure_' + x
150 def __init__(self
, **kw
):
151 ConfigurationContext
.__init
__(self
, **kw
)
154 class remote(BuildContext
):
158 def get_ssh_hosts(self
):
160 for v
in Context
.g_module
.variants
:
161 self
.env
.HOST
= self
.login_to_host(self
.variant_to_login(v
))
162 cmd
= Utils
.subst_vars('${SSH_KEYSCAN} -t rsa,ecdsa ${HOST}', self
.env
)
163 out
, err
= self
.cmd_and_log(cmd
, output
=Context
.BOTH
, quiet
=Context
.BOTH
)
164 lst
.append(out
.strip())
167 def setup_private_ssh_key(self
):
169 When WAF_SSH_KEY points to a private key, a .ssh directory will be created in the build directory
170 Make sure that the ssh key does not prompt for a password
172 key
= os
.environ
.get('WAF_SSH_KEY', '')
175 if not os
.path
.isfile(key
):
176 self
.fatal('Key in WAF_SSH_KEY must point to a valid file')
177 self
.ssh_dir
= os
.path
.join(self
.path
.abspath(), 'build', '.ssh')
178 self
.ssh_hosts
= os
.path
.join(self
.ssh_dir
, 'known_hosts')
179 self
.ssh_key
= os
.path
.join(self
.ssh_dir
, os
.path
.basename(key
))
180 self
.ssh_config
= os
.path
.join(self
.ssh_dir
, 'config')
181 for x
in self
.ssh_hosts
, self
.ssh_key
, self
.ssh_config
:
182 if not os
.path
.isfile(x
):
183 if not os
.path
.isdir(self
.ssh_dir
):
184 os
.makedirs(self
.ssh_dir
)
185 Utils
.writef(self
.ssh_key
, Utils
.readf(key
), 'wb')
186 os
.chmod(self
.ssh_key
, 448)
188 Utils
.writef(self
.ssh_hosts
, '\n'.join(self
.get_ssh_hosts()))
189 os
.chmod(self
.ssh_key
, 448)
191 Utils
.writef(self
.ssh_config
, 'UserKnownHostsFile %s' % self
.ssh_hosts
, 'wb')
192 os
.chmod(self
.ssh_config
, 448)
193 self
.env
.SSH_OPTS
= ['-F', self
.ssh_config
, '-i', self
.ssh_key
]
194 self
.env
.append_value('RSYNC_SEND_OPTS', '--exclude=build/.ssh')
196 def skip_unbuildable_variant(self
):
197 # skip variants that cannot be built on this OS
198 for k
in Options
.commands
:
199 a
, _
, b
= k
.partition('_')
200 if b
in Context
.g_module
.variants
:
201 c
, _
, _
= b
.partition('_')
202 if c
!= Utils
.unversioned_sys_platform():
203 Options
.commands
.remove(k
)
205 def login_to_host(self
, login
):
206 return re
.sub(r
'(\w+@)', '', login
)
208 def variant_to_login(self
, variant
):
209 """linux_32_debug -> search env.LINUX_32 and then env.LINUX"""
210 x
= variant
[:variant
.rfind('_')]
211 ret
= os
.environ
.get('REMOTE_' + x
.upper(), '')
214 ret
= os
.environ
.get('REMOTE_' + x
.upper(), '')
216 ret
= '%s@localhost' % getpass
.getuser()
222 self
.skip_unbuildable_variant()
224 BuildContext
.execute(self
)
227 self
.top_dir
= os
.path
.abspath(Context
.g_module
.top
)
228 self
.srcnode
= self
.root
.find_node(self
.top_dir
)
229 self
.path
= self
.srcnode
231 self
.out_dir
= os
.path
.join(self
.top_dir
, Context
.g_module
.out
)
232 self
.bldnode
= self
.root
.make_node(self
.out_dir
)
235 self
.env
= ConfigSet
.ConfigSet()
237 def extract_groups_of_builds(self
):
238 """Return a dict mapping each variants to the commands to build"""
240 for x
in reversed(Options
.commands
):
241 _
, _
, variant
= x
.partition('_')
242 if variant
in Context
.g_module
.variants
:
244 dct
= self
.vgroups
[variant
]
246 dct
= self
.vgroups
[variant
] = OrderedDict()
248 dct
[variant
].append(x
)
251 Options
.commands
.remove(x
)
253 def custom_options(self
, login
):
255 return Context
.g_module
.host_options
[login
]
256 except (AttributeError, KeyError):
259 def recurse(self
, *k
, **kw
):
260 self
.env
.RSYNC
= getattr(Context
.g_module
, 'rsync', 'rsync -a --chmod=u+rwx')
261 self
.env
.SSH
= getattr(Context
.g_module
, 'ssh', 'ssh')
262 self
.env
.SSH_KEYSCAN
= getattr(Context
.g_module
, 'ssh_keyscan', 'ssh-keyscan')
264 self
.env
.WAF
= getattr(Context
.g_module
, 'waf')
265 except AttributeError:
269 self
.fatal('Put a waf file in the directory (./waf-light --tools=remote)')
271 self
.env
.WAF
= './waf'
273 self
.extract_groups_of_builds()
274 self
.setup_private_ssh_key()
275 for k
, v
in self
.vgroups
.items():
276 task
= self(rule
=rsync_and_ssh
, always
=True)
277 task
.env
.login
= self
.variant_to_login(k
)
279 task
.env
.commands
= []
280 for opt
, value
in v
.items():
281 task
.env
.commands
+= value
282 task
.env
.variant
= task
.env
.commands
[0].partition('_')[2]
283 for opt
, value
in self
.custom_options(k
):
284 task
.env
[opt
] = value
285 self
.jobs
= len(self
.vgroups
)
287 def make_mkdir_command(self
, task
):
288 return Utils
.subst_vars('${SSH} ${SSH_OPTS} ${login} "rm -fr ${remote_dir} && mkdir -p ${remote_dir}"', task
.env
)
290 def make_send_command(self
, task
):
291 return Utils
.subst_vars('${RSYNC} ${RSYNC_SEND_OPTS} -e "${SSH} ${SSH_OPTS}" ${local_dir} ${login}:${remote_dir}', task
.env
)
293 def make_exec_command(self
, task
):
294 txt
= '''${SSH} ${SSH_OPTS} ${login} "cd ${remote_dir} && ${WAF} ${commands}"'''
295 return Utils
.subst_vars(txt
, task
.env
)
297 def make_save_command(self
, task
):
298 return Utils
.subst_vars('${RSYNC} ${RSYNC_SAVE_OPTS} -e "${SSH} ${SSH_OPTS}" ${login}:${remote_dir_variant} ${build_dir}', task
.env
)
300 def rsync_and_ssh(task
):
305 bld
= task
.generator
.bld
307 task
.env
.user
, _
, _
= task
.env
.login
.partition('@')
308 task
.env
.hdir
= Utils
.to_hex(Utils
.h_list((task
.generator
.path
.abspath(), task
.env
.variant
)))
309 task
.env
.remote_dir
= '~%s/wafremote/%s' % (task
.env
.user
, task
.env
.hdir
)
310 task
.env
.local_dir
= bld
.srcnode
.abspath() + '/'
312 task
.env
.remote_dir_variant
= '%s/%s/%s' % (task
.env
.remote_dir
, Context
.g_module
.out
, task
.env
.variant
)
313 task
.env
.build_dir
= bld
.bldnode
.abspath()
315 ret
= task
.exec_command(bld
.make_mkdir_command(task
))
318 ret
= task
.exec_command(bld
.make_send_command(task
))
321 ret
= task
.exec_command(bld
.make_exec_command(task
))
324 ret
= task
.exec_command(bld
.make_save_command(task
))