ctdb-daemon: Use ctdb_parse_node_address() in ctdbd
[samba4-gss.git] / third_party / waf / waflib / extras / remote.py
blobf43b600f02374dd28ac7537583330b8b4ac768bf
1 #!/usr/bin/env python
2 # encoding: utf-8
3 # Remote Builds tool using rsync+ssh
5 __author__ = "Jérôme Carretero <cJ-waf@zougloub.eu>"
6 __copyright__ = "Jérôme Carretero, 2013"
8 """
9 Simple Remote Builds
10 ********************
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
14 remote builds.
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.
28 Usage
29 =====
31 1. Set your wscript file so it includes a list of variants,
32 e.g.::
34 from waflib import Utils
35 top = '.'
36 out = 'build'
38 variants = [
39 'linux_64_debug',
40 'linux_64_release',
41 'linux_32_debug',
42 'linux_32_release',
45 from waflib.extras import remote
47 def options(opt):
48 # normal stuff from here on
49 opt.load('compiler_c')
51 def configure(conf):
52 if not conf.variant:
53 return
54 # normal stuff from here on
55 conf.load('compiler_c')
57 def build(bld):
58 if not bld.variant:
59 return
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
65 directory
67 .. code:: bash
69 ./waf-light --tools=remote
71 3. Set the host names to access the hosts:
73 .. code:: bash
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:
82 .. code:: bash
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).
93 .. code:: bash
95 export WAF_SSH_KEY=~/foo.rsa
97 5. Perform the build:
99 .. code:: bash
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
114 is_remote = False
115 if '--remote' in sys.argv:
116 is_remote = True
117 sys.argv.remove('--remote')
119 class init(Context.Context):
121 Generates the *_all commands
123 cmd = 'init'
124 fun = 'init'
125 def execute(self):
126 for x in list(Context.g_module.variants):
127 self.make_variant(x)
128 lst = ['remote']
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))
134 else:
135 lst.append(k)
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()
142 class tmp(y):
143 cmd = name + '_' + x
144 fun = 'build'
145 variant = x
146 class tmp(ConfigurationContext):
147 cmd = 'configure_' + x
148 fun = 'configure'
149 variant = x
150 def __init__(self, **kw):
151 ConfigurationContext.__init__(self, **kw)
152 self.setenv(x)
154 class remote(BuildContext):
155 cmd = 'remote'
156 fun = 'build'
158 def get_ssh_hosts(self):
159 lst = []
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())
165 return lst
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', '')
173 if not key:
174 return
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(), '')
212 if not ret:
213 x = x[:x.find('_')]
214 ret = os.environ.get('REMOTE_' + x.upper(), '')
215 if not ret:
216 ret = '%s@localhost' % getpass.getuser()
217 return ret
219 def execute(self):
220 global is_remote
221 if not is_remote:
222 self.skip_unbuildable_variant()
223 else:
224 BuildContext.execute(self)
226 def restore(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)
233 self.bldnode.mkdir()
235 self.env = ConfigSet.ConfigSet()
237 def extract_groups_of_builds(self):
238 """Return a dict mapping each variants to the commands to build"""
239 self.vgroups = {}
240 for x in reversed(Options.commands):
241 _, _, variant = x.partition('_')
242 if variant in Context.g_module.variants:
243 try:
244 dct = self.vgroups[variant]
245 except KeyError:
246 dct = self.vgroups[variant] = OrderedDict()
247 try:
248 dct[variant].append(x)
249 except KeyError:
250 dct[variant] = [x]
251 Options.commands.remove(x)
253 def custom_options(self, login):
254 try:
255 return Context.g_module.host_options[login]
256 except (AttributeError, KeyError):
257 return {}
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')
263 try:
264 self.env.WAF = getattr(Context.g_module, 'waf')
265 except AttributeError:
266 try:
267 os.stat('waf')
268 except KeyError:
269 self.fatal('Put a waf file in the directory (./waf-light --tools=remote)')
270 else:
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):
302 # remove a warning
303 task.uid_ = id(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))
316 if ret:
317 return ret
318 ret = task.exec_command(bld.make_send_command(task))
319 if ret:
320 return ret
321 ret = task.exec_command(bld.make_exec_command(task))
322 if ret:
323 return ret
324 ret = task.exec_command(bld.make_save_command(task))
325 if ret:
326 return ret