waf

FORK: waf with some random patches
git clone https://git.neptards.moe/neptards/waf.git
Log | Files | Refs | README

distnet.py (11589B)


      1 #! /usr/bin/env python
      2 # encoding: utf-8
      3 
      4 """
      5 waf-powered distributed network builds, with a network cache.
      6 
      7 Caching files from a server has advantages over a NFS/Samba shared folder:
      8 
      9 - builds are much faster because they use local files
     10 - builds just continue to work in case of a network glitch
     11 - permissions are much simpler to manage
     12 """
     13 
     14 import os, urllib, tarfile, re, shutil, tempfile, sys
     15 from collections import OrderedDict
     16 from waflib import Context, Utils, Logs
     17 
     18 try:
     19 	from urllib.parse import urlencode
     20 except ImportError:
     21 	urlencode = urllib.urlencode
     22 
     23 def safe_urlencode(data):
     24 	x = urlencode(data)
     25 	try:
     26 		x = x.encode('utf-8')
     27 	except Exception:
     28 		pass
     29 	return x
     30 
     31 try:
     32 	from urllib.error import URLError
     33 except ImportError:
     34 	from urllib2 import URLError
     35 
     36 try:
     37 	from urllib.request import Request, urlopen
     38 except ImportError:
     39 	from urllib2 import Request, urlopen
     40 
     41 DISTNETCACHE = os.environ.get('DISTNETCACHE', '/tmp/distnetcache')
     42 DISTNETSERVER = os.environ.get('DISTNETSERVER', 'http://localhost:8000/cgi-bin/')
     43 TARFORMAT = 'w:bz2'
     44 TIMEOUT = 60
     45 REQUIRES = 'requires.txt'
     46 
     47 re_com = re.compile(r'\s*#.*', re.M)
     48 
     49 def total_version_order(num):
     50 	lst = num.split('.')
     51 	template = '%10s' * len(lst)
     52 	ret = template % tuple(lst)
     53 	return ret
     54 
     55 def get_distnet_cache():
     56 	return getattr(Context.g_module, 'DISTNETCACHE', DISTNETCACHE)
     57 
     58 def get_server_url():
     59 	return getattr(Context.g_module, 'DISTNETSERVER', DISTNETSERVER)
     60 
     61 def get_download_url():
     62 	return '%s/download.py' % get_server_url()
     63 
     64 def get_upload_url():
     65 	return '%s/upload.py' % get_server_url()
     66 
     67 def get_resolve_url():
     68 	return '%s/resolve.py' % get_server_url()
     69 
     70 def send_package_name():
     71 	out = getattr(Context.g_module, 'out', 'build')
     72 	pkgfile = '%s/package_to_upload.tarfile' % out
     73 	return pkgfile
     74 
     75 class package(Context.Context):
     76 	fun = 'package'
     77 	cmd = 'package'
     78 
     79 	def execute(self):
     80 		try:
     81 			files = self.files
     82 		except AttributeError:
     83 			files = self.files = []
     84 
     85 		Context.Context.execute(self)
     86 		pkgfile = send_package_name()
     87 		if not pkgfile in files:
     88 			if not REQUIRES in files:
     89 				files.append(REQUIRES)
     90 			self.make_tarfile(pkgfile, files, add_to_package=False)
     91 
     92 	def make_tarfile(self, filename, files, **kw):
     93 		if kw.get('add_to_package', True):
     94 			self.files.append(filename)
     95 
     96 		with tarfile.open(filename, TARFORMAT) as tar:
     97 			endname = os.path.split(filename)[-1]
     98 			endname = endname.split('.')[0] + '/'
     99 			for x in files:
    100 				tarinfo = tar.gettarinfo(x, x)
    101 				tarinfo.uid   = tarinfo.gid   = 0
    102 				tarinfo.uname = tarinfo.gname = 'root'
    103 				tarinfo.size = os.stat(x).st_size
    104 
    105 				# TODO - more archive creation options?
    106 				if kw.get('bare', True):
    107 					tarinfo.name = os.path.split(x)[1]
    108 				else:
    109 					tarinfo.name = endname + x # todo, if tuple, then..
    110 				Logs.debug('distnet: adding %r to %s', tarinfo.name, filename)
    111 				with open(x, 'rb') as f:
    112 					tar.addfile(tarinfo, f)
    113 		Logs.info('Created %s', filename)
    114 
    115 class publish(Context.Context):
    116 	fun = 'publish'
    117 	cmd = 'publish'
    118 	def execute(self):
    119 		if hasattr(Context.g_module, 'publish'):
    120 			Context.Context.execute(self)
    121 		mod = Context.g_module
    122 
    123 		rfile = getattr(self, 'rfile', send_package_name())
    124 		if not os.path.isfile(rfile):
    125 			self.fatal('Create the release file with "waf release" first! %r' % rfile)
    126 
    127 		fdata = Utils.readf(rfile, m='rb')
    128 		data = safe_urlencode([('pkgdata', fdata), ('pkgname', mod.APPNAME), ('pkgver', mod.VERSION)])
    129 
    130 		req = Request(get_upload_url(), data)
    131 		response = urlopen(req, timeout=TIMEOUT)
    132 		data = response.read().strip()
    133 
    134 		if sys.hexversion>0x300000f:
    135 			data = data.decode('utf-8')
    136 
    137 		if data != 'ok':
    138 			self.fatal('Could not publish the package %r' % data)
    139 
    140 class constraint(object):
    141 	def __init__(self, line=''):
    142 		self.required_line = line
    143 		self.info = []
    144 
    145 		line = line.strip()
    146 		if not line:
    147 			return
    148 
    149 		lst = line.split(',')
    150 		if lst:
    151 			self.pkgname = lst[0]
    152 			self.required_version = lst[1]
    153 			for k in lst:
    154 				a, b, c = k.partition('=')
    155 				if a and c:
    156 					self.info.append((a, c))
    157 	def __str__(self):
    158 		buf = []
    159 		buf.append(self.pkgname)
    160 		buf.append(self.required_version)
    161 		for k in self.info:
    162 			buf.append('%s=%s' % k)
    163 		return ','.join(buf)
    164 
    165 	def __repr__(self):
    166 		return "requires %s-%s" % (self.pkgname, self.required_version)
    167 
    168 	def human_display(self, pkgname, pkgver):
    169 		return '%s-%s requires %s-%s' % (pkgname, pkgver, self.pkgname, self.required_version)
    170 
    171 	def why(self):
    172 		ret = []
    173 		for x in self.info:
    174 			if x[0] == 'reason':
    175 				ret.append(x[1])
    176 		return ret
    177 
    178 	def add_reason(self, reason):
    179 		self.info.append(('reason', reason))
    180 
    181 def parse_constraints(text):
    182 	assert(text is not None)
    183 	constraints = []
    184 	text = re.sub(re_com, '', text)
    185 	lines = text.splitlines()
    186 	for line in lines:
    187 		line = line.strip()
    188 		if not line:
    189 			continue
    190 		constraints.append(constraint(line))
    191 	return constraints
    192 
    193 def list_package_versions(cachedir, pkgname):
    194 	pkgdir = os.path.join(cachedir, pkgname)
    195 	try:
    196 		versions = os.listdir(pkgdir)
    197 	except OSError:
    198 		return []
    199 	versions.sort(key=total_version_order)
    200 	versions.reverse()
    201 	return versions
    202 
    203 class package_reader(Context.Context):
    204 	cmd = 'solver'
    205 	fun = 'solver'
    206 
    207 	def __init__(self, **kw):
    208 		Context.Context.__init__(self, **kw)
    209 
    210 		self.myproject = getattr(Context.g_module, 'APPNAME', 'project')
    211 		self.myversion = getattr(Context.g_module, 'VERSION', '1.0')
    212 		self.cache_constraints = {}
    213 		self.constraints = []
    214 
    215 	def compute_dependencies(self, filename=REQUIRES):
    216 		text = Utils.readf(filename)
    217 		data = safe_urlencode([('text', text)])
    218 
    219 		if '--offline' in sys.argv:
    220 			self.constraints = self.local_resolve(text)
    221 		else:
    222 			req = Request(get_resolve_url(), data)
    223 			try:
    224 				response = urlopen(req, timeout=TIMEOUT)
    225 			except URLError as e:
    226 				Logs.warn('The package server is down! %r', e)
    227 				self.constraints = self.local_resolve(text)
    228 			else:
    229 				ret = response.read()
    230 				try:
    231 					ret = ret.decode('utf-8')
    232 				except Exception:
    233 					pass
    234 				self.trace(ret)
    235 				self.constraints = parse_constraints(ret)
    236 		self.check_errors()
    237 
    238 	def check_errors(self):
    239 		errors = False
    240 		for c in self.constraints:
    241 			if not c.required_version:
    242 				errors = True
    243 
    244 				reasons = c.why()
    245 				if len(reasons) == 1:
    246 					Logs.error('%s but no matching package could be found in this repository', reasons[0])
    247 				else:
    248 					Logs.error('Conflicts on package %r:', c.pkgname)
    249 					for r in reasons:
    250 						Logs.error('  %s', r)
    251 		if errors:
    252 			self.fatal('The package requirements cannot be satisfied!')
    253 
    254 	def load_constraints(self, pkgname, pkgver, requires=REQUIRES):
    255 		try:
    256 			return self.cache_constraints[(pkgname, pkgver)]
    257 		except KeyError:
    258 			text = Utils.readf(os.path.join(get_distnet_cache(), pkgname, pkgver, requires))
    259 			ret = parse_constraints(text)
    260 			self.cache_constraints[(pkgname, pkgver)] = ret
    261 			return ret
    262 
    263 	def apply_constraint(self, domain, constraint):
    264 		vname = constraint.required_version.replace('*', '.*')
    265 		rev = re.compile(vname, re.M)
    266 		ret = [x for x in domain if rev.match(x)]
    267 		return ret
    268 
    269 	def trace(self, *k):
    270 		if getattr(self, 'debug', None):
    271 			Logs.error(*k)
    272 
    273 	def solve(self, packages_to_versions={}, packages_to_constraints={}, pkgname='', pkgver='', todo=[], done=[]):
    274 		# breadth first search
    275 		n_packages_to_versions = dict(packages_to_versions)
    276 		n_packages_to_constraints = dict(packages_to_constraints)
    277 
    278 		self.trace("calling solve with %r    %r %r" % (packages_to_versions, todo, done))
    279 		done = done + [pkgname]
    280 
    281 		constraints = self.load_constraints(pkgname, pkgver)
    282 		self.trace("constraints %r" % constraints)
    283 
    284 		for k in constraints:
    285 			try:
    286 				domain = n_packages_to_versions[k.pkgname]
    287 			except KeyError:
    288 				domain = list_package_versions(get_distnet_cache(), k.pkgname)
    289 
    290 
    291 			self.trace("constraints?")
    292 			if not k.pkgname in done:
    293 				todo = todo + [k.pkgname]
    294 
    295 			self.trace("domain before %s -> %s, %r" % (pkgname, k.pkgname, domain))
    296 
    297 			# apply the constraint
    298 			domain = self.apply_constraint(domain, k)
    299 
    300 			self.trace("domain after %s -> %s, %r" % (pkgname, k.pkgname, domain))
    301 
    302 			n_packages_to_versions[k.pkgname] = domain
    303 
    304 			# then store the constraint applied
    305 			constraints = list(packages_to_constraints.get(k.pkgname, []))
    306 			constraints.append((pkgname, pkgver, k))
    307 			n_packages_to_constraints[k.pkgname] = constraints
    308 
    309 			if not domain:
    310 				self.trace("no domain while processing constraint %r from %r %r" % (domain, pkgname, pkgver))
    311 				return (n_packages_to_versions, n_packages_to_constraints)
    312 
    313 		# next package on the todo list
    314 		if not todo:
    315 			return (n_packages_to_versions, n_packages_to_constraints)
    316 
    317 		n_pkgname = todo[0]
    318 		n_pkgver = n_packages_to_versions[n_pkgname][0]
    319 		tmp = dict(n_packages_to_versions)
    320 		tmp[n_pkgname] = [n_pkgver]
    321 
    322 		self.trace("fixed point %s" % n_pkgname)
    323 
    324 		return self.solve(tmp, n_packages_to_constraints, n_pkgname, n_pkgver, todo[1:], done)
    325 
    326 	def get_results(self):
    327 		return '\n'.join([str(c) for c in self.constraints])
    328 
    329 	def solution_to_constraints(self, versions, constraints):
    330 		solution = []
    331 		for p in versions:
    332 			c = constraint()
    333 			solution.append(c)
    334 
    335 			c.pkgname = p
    336 			if versions[p]:
    337 				c.required_version = versions[p][0]
    338 			else:
    339 				c.required_version = ''
    340 			for (from_pkgname, from_pkgver, c2) in constraints.get(p, ''):
    341 				c.add_reason(c2.human_display(from_pkgname, from_pkgver))
    342 		return solution
    343 
    344 	def local_resolve(self, text):
    345 		self.cache_constraints[(self.myproject, self.myversion)] = parse_constraints(text)
    346 		p2v = OrderedDict({self.myproject: [self.myversion]})
    347 		(versions, constraints) = self.solve(p2v, {}, self.myproject, self.myversion, [])
    348 		return self.solution_to_constraints(versions, constraints)
    349 
    350 	def download_to_file(self, pkgname, pkgver, subdir, tmp):
    351 		data = safe_urlencode([('pkgname', pkgname), ('pkgver', pkgver), ('pkgfile', subdir)])
    352 		req = urlopen(get_download_url(), data, timeout=TIMEOUT)
    353 		with open(tmp, 'wb') as f:
    354 			while True:
    355 				buf = req.read(8192)
    356 				if not buf:
    357 					break
    358 				f.write(buf)
    359 
    360 	def extract_tar(self, subdir, pkgdir, tmpfile):
    361 		with tarfile.open(tmpfile) as f:
    362 			temp = tempfile.mkdtemp(dir=pkgdir)
    363 			try:
    364 				f.extractall(temp)
    365 				os.rename(temp, os.path.join(pkgdir, subdir))
    366 			finally:
    367 				try:
    368 					shutil.rmtree(temp)
    369 				except Exception:
    370 					pass
    371 
    372 	def get_pkg_dir(self, pkgname, pkgver, subdir):
    373 		pkgdir = os.path.join(get_distnet_cache(), pkgname, pkgver)
    374 		if not os.path.isdir(pkgdir):
    375 			os.makedirs(pkgdir)
    376 
    377 		target = os.path.join(pkgdir, subdir)
    378 
    379 		if os.path.exists(target):
    380 			return target
    381 
    382 		(fd, tmp) = tempfile.mkstemp(dir=pkgdir)
    383 		try:
    384 			os.close(fd)
    385 			self.download_to_file(pkgname, pkgver, subdir, tmp)
    386 			if subdir == REQUIRES:
    387 				os.rename(tmp, target)
    388 			else:
    389 				self.extract_tar(subdir, pkgdir, tmp)
    390 		finally:
    391 			try:
    392 				os.remove(tmp)
    393 			except OSError:
    394 				pass
    395 
    396 		return target
    397 
    398 	def __iter__(self):
    399 		if not self.constraints:
    400 			self.compute_dependencies()
    401 		for x in self.constraints:
    402 			if x.pkgname == self.myproject:
    403 				continue
    404 			yield x
    405 
    406 	def execute(self):
    407 		self.compute_dependencies()
    408 
    409 packages = package_reader()
    410 
    411 def load_tools(ctx, extra):
    412 	global packages
    413 	for c in packages:
    414 		packages.get_pkg_dir(c.pkgname, c.required_version, extra)
    415 		noarchdir = packages.get_pkg_dir(c.pkgname, c.required_version, 'noarch')
    416 		for x in os.listdir(noarchdir):
    417 			if x.startswith('waf_') and x.endswith('.py'):
    418 				ctx.load([x.rstrip('.py')], tooldir=[noarchdir])
    419 
    420 def options(opt):
    421 	opt.add_option('--offline', action='store_true')
    422 	packages.execute()
    423 	load_tools(opt, REQUIRES)
    424 
    425 def configure(conf):
    426 	load_tools(conf, conf.variant)
    427 
    428 def build(bld):
    429 	load_tools(bld, bld.variant)
    430