gooderp18绿色标准版
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

507 lines
21KB

  1. #!/usr/bin/env python3
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. import argparse
  4. import logging
  5. import os
  6. import pexpect
  7. import shutil
  8. import subprocess
  9. import sys
  10. import tempfile
  11. import textwrap
  12. import time
  13. import traceback
  14. from pathlib import Path
  15. from xmlrpc import client as xmlrpclib
  16. from glob import glob
  17. #----------------------------------------------------------
  18. # Utils
  19. #----------------------------------------------------------
  20. ROOTDIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
  21. TSTAMP = time.strftime("%Y%m%d", time.gmtime())
  22. TSEC = time.strftime("%H%M%S", time.gmtime())
  23. # Get some variables from release.py
  24. version = ...
  25. version_info = ...
  26. nt_service_name = ...
  27. exec(open(os.path.join(ROOTDIR, 'odoo', 'release.py'), 'rb').read())
  28. VERSION = version.split('-')[0].replace('saas~', '')
  29. GPGPASSPHRASE = os.getenv('GPGPASSPHRASE')
  30. GPGID = os.getenv('GPGID')
  31. DOCKERVERSION = VERSION.replace('+', '')
  32. INSTALL_TIMEOUT = 600
  33. DOCKERUSER = """
  34. RUN mkdir /var/lib/odoo && \
  35. groupadd -g %(group_id)s odoo && \
  36. useradd -u %(user_id)s -g odoo odoo -d /var/lib/odoo && \
  37. mkdir /data && \
  38. chown odoo:odoo /var/lib/odoo /data
  39. USER odoo
  40. """ % {'group_id': os.getgid(), 'user_id': os.getuid()}
  41. class OdooTestTimeoutError(Exception):
  42. pass
  43. class OdooTestError(Exception):
  44. pass
  45. def run_cmd(cmd, chdir=None, timeout=None):
  46. logging.info("Running command: '%s'", ' '.join(cmd))
  47. return subprocess.run(cmd, cwd=chdir, timeout=timeout)
  48. def _rpc_count_modules(addr='http://127.0.0.1', port=8069, dbname='mycompany'):
  49. time.sleep(5)
  50. uid = xmlrpclib.ServerProxy('%s:%s/xmlrpc/2/common' % (addr, port)).authenticate(
  51. dbname, 'admin', 'admin', {}
  52. )
  53. modules = xmlrpclib.ServerProxy('%s:%s/xmlrpc/2/object' % (addr, port)).execute(
  54. dbname, uid, 'admin', 'ir.module.module', 'search', [('state', '=', 'installed')]
  55. )
  56. if len(modules) > 1:
  57. time.sleep(1)
  58. toinstallmodules = xmlrpclib.ServerProxy('%s:%s/xmlrpc/2/object' % (addr, port)).execute(
  59. dbname, uid, 'admin', 'ir.module.module', 'search', [('state', '=', 'to install')]
  60. )
  61. if toinstallmodules:
  62. logging.error("Package test: FAILED. Not able to install dependencies of base.")
  63. raise OdooTestError("Installation of package failed")
  64. else:
  65. logging.info("Package test: successfuly installed %s modules" % len(modules))
  66. else:
  67. logging.error("Package test: FAILED. Not able to install base.")
  68. raise OdooTestError("Package test: FAILED. Not able to install base.")
  69. def publish(args, pub_type, extensions):
  70. """Publish builded package (move builded files and generate a symlink to the latests)
  71. :args: parsed program args
  72. :pub_type: one of [deb, rpm, src, exe]
  73. :extensions: list of extensions to publish
  74. :returns: published files
  75. """
  76. def _publish(release):
  77. build_path = os.path.join(args.build_dir, release)
  78. filename = release.split(os.path.sep)[-1]
  79. release_dir = os.path.join(args.pub, pub_type)
  80. release_path = os.path.join(release_dir, filename)
  81. os.renames(build_path, release_path)
  82. # Latest/symlink handler
  83. release_abspath = os.path.abspath(release_path)
  84. latest_abspath = release_abspath.replace(TSTAMP, 'latest')
  85. if os.path.islink(latest_abspath):
  86. os.unlink(latest_abspath)
  87. os.symlink(release_abspath, latest_abspath)
  88. return release_path
  89. published = []
  90. for extension in extensions:
  91. release = glob("%s/odoo_*.%s" % (args.build_dir, extension))
  92. if release:
  93. published.append(_publish(release[0]))
  94. return published
  95. # ---------------------------------------------------------
  96. # Generates Packages, Sources and Release files of debian package
  97. # ---------------------------------------------------------
  98. def gen_deb_package(args, published_files):
  99. # Executes command to produce file_name in path, and moves it to args.pub/deb
  100. def _gen_file(args, command, file_name, path):
  101. cur_tmp_file_path = os.path.join(path, file_name)
  102. with open(cur_tmp_file_path, 'w') as out:
  103. subprocess.call(command, stdout=out, cwd=path)
  104. shutil.copy(cur_tmp_file_path, os.path.join(args.pub, 'deb', file_name))
  105. # Copy files to a temp directory (required because the working directory must contain only the
  106. # files of the last release)
  107. temp_path = tempfile.mkdtemp(suffix='debPackages')
  108. for pub_file_path in published_files:
  109. shutil.copy(pub_file_path, temp_path)
  110. commands = [
  111. (['dpkg-scanpackages', '--multiversion', '.'], "Packages"), # Generate Packages file
  112. (['dpkg-scansources', '.'], "Sources"), # Generate Sources file
  113. (['apt-ftparchive', 'release', '.'], "Release") # Generate Release file
  114. ]
  115. # Generate files
  116. for command in commands:
  117. _gen_file(args, command[0], command[-1], temp_path)
  118. # Remove temp directory
  119. shutil.rmtree(temp_path)
  120. if args.sign:
  121. # Generate Release.gpg (= signed Release)
  122. # Options -abs: -a (Create ASCII armored output), -b (Make a detach signature), -s (Make a signature)
  123. subprocess.call(['gpg', '--default-key', GPGID, '--passphrase', GPGPASSPHRASE, '--yes', '-abs', '--no-tty', '-o', 'Release.gpg', 'Release'], cwd=os.path.join(args.pub, 'deb'))
  124. # ---------------------------------------------------------
  125. # Generates an RPM repo
  126. # ---------------------------------------------------------
  127. def rpm_sign(args, file_name):
  128. """Genereate a rpm repo in publish directory"""
  129. # Sign the RPM
  130. rpmsign = pexpect.spawn('/bin/bash', ['-c', 'rpm --resign %s' % file_name], cwd=os.path.join(args.pub, 'rpm'))
  131. rpmsign.expect_exact('Enter passphrase: ')
  132. rpmsign.send(GPGPASSPHRASE + '\r\n')
  133. rpmsign.expect(pexpect.EOF)
  134. def _prepare_build_dir(args, win32=False, move_addons=True):
  135. """Copy files to the build directory"""
  136. logging.info('Preparing build dir "%s"', args.build_dir)
  137. cmd = ['rsync', '-a', '--delete', '--exclude', '.git', '--exclude', '*.pyc', '--exclude', '*.pyo']
  138. if win32 is False:
  139. cmd += ['--exclude', 'setup/win32']
  140. run_cmd(cmd + ['%s/' % args.odoo_dir, args.build_dir])
  141. if not move_addons:
  142. return
  143. for addon_path in glob(os.path.join(args.build_dir, 'addons/*')):
  144. if args.blacklist is None or os.path.basename(addon_path) not in args.blacklist:
  145. try:
  146. shutil.move(addon_path, os.path.join(args.build_dir, 'odoo/addons'))
  147. except shutil.Error as e:
  148. logging.warning("Warning '%s' while moving addon '%s", e, addon_path)
  149. if addon_path.startswith(args.build_dir) and os.path.isdir(addon_path):
  150. logging.info("Removing '{}'".format(addon_path))
  151. try:
  152. shutil.rmtree(addon_path)
  153. except shutil.Error as rm_error:
  154. logging.warning("Cannot remove '{}': {}".format(addon_path, rm_error))
  155. # Docker stuffs
  156. class Docker():
  157. """Base Docker class. Must be inherited by specific Docker builder class"""
  158. arch = None
  159. def __init__(self, args):
  160. """
  161. :param args: argparse parsed arguments
  162. """
  163. self.args = args
  164. self.tag = 'odoo-%s-%s-nightly-tests' % (DOCKERVERSION, self.arch)
  165. self.container_name = None
  166. self.exposed_port = None
  167. docker_templates = {
  168. 'tgz': os.path.join(args.build_dir, 'setup/package.dfsrc'),
  169. 'deb': os.path.join(args.build_dir, 'setup/package.dfdebian'),
  170. 'rpm': os.path.join(args.build_dir, 'setup/package.dffedora'),
  171. 'win': os.path.join(args.build_dir, 'setup/package.dfwine'),
  172. }
  173. self.docker_template = Path(docker_templates[self.arch]).read_text(encoding='utf-8').replace('USER odoo', DOCKERUSER)
  174. self.test_log_file = '/data/src/test-%s.log' % self.arch
  175. self.docker_dir = Path(self.args.build_dir) / 'docker'
  176. if not self.docker_dir.exists():
  177. self.docker_dir.mkdir()
  178. self.build_image()
  179. def build_image(self):
  180. """Build the dockerimage by copying Dockerfile into build_dir/docker"""
  181. docker_file = self.docker_dir / 'Dockerfile'
  182. docker_file.write_text(self.docker_template)
  183. shutil.copy(os.path.join(self.args.build_dir, 'requirements.txt'), self.docker_dir)
  184. run_cmd(["docker", "build", "--rm=True", "-t", self.tag, "."], chdir=self.docker_dir, timeout=1200).check_returncode()
  185. shutil.rmtree(self.docker_dir)
  186. def run(self, cmd, build_dir, container_name, user='odoo', exposed_port=None, detach=False, timeout=None):
  187. self.container_name = container_name
  188. docker_cmd = [
  189. "docker",
  190. "run",
  191. "--user=%s" % user,
  192. "--name=%s" % container_name,
  193. "--rm",
  194. "--volume=%s:/data/src" % build_dir
  195. ]
  196. if exposed_port:
  197. docker_cmd.extend(['-p', '127.0.0.1:%s:%s' % (exposed_port, exposed_port)])
  198. self.exposed_port = exposed_port
  199. if detach:
  200. docker_cmd.append('-d')
  201. # preserve logs in case of detached docker container
  202. cmd = '(%s) > %s 2>&1' % (cmd, self.test_log_file)
  203. docker_cmd.extend([
  204. self.tag,
  205. "/bin/bash",
  206. "-c",
  207. "cd /data/src && %s" % cmd
  208. ])
  209. run_cmd(docker_cmd, timeout=timeout).check_returncode()
  210. def is_running(self):
  211. dinspect = subprocess.run(['docker', 'container', 'inspect', self.container_name], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
  212. return True if dinspect.returncode == 0 else False
  213. def stop(self):
  214. run_cmd(["docker", "stop", self.container_name]).check_returncode()
  215. def test_odoo(self):
  216. logging.info('Starting to test Odoo install test')
  217. start_time = time.time()
  218. while self.is_running() and (time.time() - start_time) < INSTALL_TIMEOUT:
  219. time.sleep(5)
  220. if os.path.exists(os.path.join(args.build_dir, 'odoo.pid')):
  221. try:
  222. _rpc_count_modules(port=self.exposed_port)
  223. finally:
  224. self.stop()
  225. return
  226. if self.is_running():
  227. self.stop()
  228. raise OdooTestTimeoutError('Odoo pid file never appeared after %s sec' % INSTALL_TIMEOUT)
  229. raise OdooTestError('Error while installing/starting Odoo after %s sec.\nSee testlogs.txt in build dir' % int(time.time() - start_time))
  230. def build(self):
  231. """To be overriden by specific builder"""
  232. pass
  233. def start_test(self):
  234. """To be overriden by specific builder"""
  235. pass
  236. class DockerTgz(Docker):
  237. """Docker class to build python src package"""
  238. arch = 'tgz'
  239. def build(self):
  240. logging.info('Start building python tgz package')
  241. self.run('python3 setup.py sdist --quiet --formats=gztar,zip', self.args.build_dir, 'odoo-src-build-%s' % TSTAMP)
  242. os.rename(glob('%s/dist/odoo-*.tar.gz' % self.args.build_dir)[0], '%s/odoo_%s.%s.tar.gz' % (self.args.build_dir, VERSION, TSTAMP))
  243. os.rename(glob('%s/dist/odoo-*.zip' % self.args.build_dir)[0], '%s/odoo_%s.%s.zip' % (self.args.build_dir, VERSION, TSTAMP))
  244. logging.info('Finished building python tgz package')
  245. def start_test(self):
  246. if not self.args.test:
  247. return
  248. logging.info('Start testing python tgz package')
  249. cmds = [
  250. 'service postgresql start',
  251. 'su postgres -s /bin/bash -c "createuser -s odoo"',
  252. 'su odoo -s /bin/bash -c "python3 -m venv /var/lib/odoo/odoovenv"',
  253. 'su odoo -s /bin/bash -c "/var/lib/odoo/odoovenv/bin/python3 -m pip install --upgrade pip"',
  254. 'su odoo -s /bin/bash -c "/var/lib/odoo/odoovenv/bin/python3 -m pip install -r /opt/release/requirements.txt"',
  255. f'su odoo -s /bin/bash -c "/var/lib/odoo/odoovenv/bin/python3 -m pip install /data/src/odoo_{VERSION}.{TSTAMP}.tar.gz"',
  256. 'su odoo -s /bin/bash -c "createdb mycompany"',
  257. 'su odoo -s /bin/bash -c "/var/lib/odoo/odoovenv/bin/odoo -d mycompany -i base --stop-after-init"',
  258. 'su odoo -s /bin/bash -c "/var/lib/odoo/odoovenv/bin/odoo -d mycompany --pidfile=/data/src/odoo.pid"',
  259. ]
  260. self.run(' && '.join(cmds), self.args.build_dir, 'odoo-src-test-%s' % TSTAMP, user='root', detach=True, exposed_port=8069, timeout=300)
  261. self.test_odoo()
  262. logging.info('Finished testing tgz package')
  263. class DockerDeb(Docker):
  264. """Docker class to build debian package"""
  265. arch = 'deb'
  266. def build(self):
  267. logging.info('Start building debian package')
  268. # Append timestamp to version for the .dsc to refer the right .tar.gz
  269. cmds = ["sed -i '1s/^.*$/odoo (%s.%s) stable; urgency=low/' debian/changelog" % (VERSION, TSTAMP)]
  270. cmds.append('dpkg-buildpackage -rfakeroot -uc -us -tc')
  271. # As the packages are built in the parent of the buildir, we move them back to build_dir
  272. cmds.append('mv ../odoo_* ./')
  273. self.run(' && '.join(cmds), self.args.build_dir, 'odoo-deb-build-%s' % TSTAMP)
  274. logging.info('Finished building debian package')
  275. def start_test(self):
  276. if not self.args.test:
  277. return
  278. logging.info('Start testing debian package')
  279. cmds = [
  280. 'service postgresql start',
  281. '/usr/bin/apt-get update -y',
  282. f'/usr/bin/apt-get install -y /data/src/odoo_{VERSION}.{TSTAMP}_all.deb',
  283. 'su odoo -s /bin/bash -c "odoo -d mycompany -i base --pidfile=/data/src/odoo.pid"',
  284. ]
  285. self.run(' && '.join(cmds), self.args.build_dir, 'odoo-deb-test-%s' % TSTAMP, user='root', detach=True, exposed_port=8069, timeout=300)
  286. self.test_odoo()
  287. logging.info('Finished testing debian package')
  288. class DockerRpm(Docker):
  289. """Docker class to build rpm package"""
  290. arch = 'rpm'
  291. def build(self):
  292. logging.info('Start building fedora rpm package')
  293. rpmbuild_dir = '/var/lib/odoo/rpmbuild'
  294. cmds = [
  295. 'cd /data/src',
  296. 'mkdir -p dist',
  297. 'rpmdev-setuptree -d',
  298. f'cp -a /data/src/setup/rpm/odoo.spec {rpmbuild_dir}/SPECS/',
  299. f'tar --transform "s/^\\./odoo-{VERSION}/" -c -z -f {rpmbuild_dir}/SOURCES/odoo-{VERSION}.tar.gz .',
  300. f'rpmbuild -bb --define="%version {VERSION}" /data/src/setup/rpm/odoo.spec',
  301. f'mv {rpmbuild_dir}/RPMS/noarch/odoo*.rpm /data/src/dist/'
  302. ]
  303. self.run(' && '.join(cmds), self.args.build_dir, f'odoo-rpm-build-{TSTAMP}')
  304. os.rename(glob('%s/dist/odoo-*.noarch.rpm' % self.args.build_dir)[0], '%s/odoo_%s.%s.rpm' % (self.args.build_dir, VERSION, TSTAMP))
  305. logging.info('Finished building fedora rpm package')
  306. def start_test(self):
  307. if not self.args.test:
  308. return
  309. logging.info('Start testing rpm package')
  310. cmds = [
  311. 'su postgres -c "/usr/bin/pg_ctl -D /var/lib/postgres/data start"',
  312. 'sleep 5',
  313. 'su postgres -c "createuser -s odoo"',
  314. 'su odoo -c "createdb mycompany"',
  315. 'dnf install -d 0 -e 0 /data/src/odoo_%s.%s.rpm -y' % (VERSION, TSTAMP),
  316. 'su odoo -s /bin/bash -c "odoo -c /etc/odoo/odoo.conf -d mycompany -i base --stop-after-init"',
  317. 'su odoo -s /bin/bash -c "odoo -c /etc/odoo/odoo.conf -d mycompany --pidfile=/data/src/odoo.pid"',
  318. ]
  319. self.run(' && '.join(cmds), args.build_dir, 'odoo-rpm-test-%s' % TSTAMP, user='root', detach=True, exposed_port=8069, timeout=300)
  320. self.test_odoo()
  321. logging.info('Finished testing rpm package')
  322. def gen_rpm_repo(self, args, rpm_filepath):
  323. pub_repodata_path = os.path.join(args.pub, 'rpm', 'repodata')
  324. # Removes the old repodata
  325. if os.path.isdir(pub_repodata_path):
  326. shutil.rmtree(pub_repodata_path)
  327. # Copy files to a temp directory (required because the working directory must contain only the
  328. # files of the last release)
  329. temp_path = tempfile.mkdtemp(suffix='rpmPackages')
  330. shutil.copy(rpm_filepath, temp_path)
  331. logging.info('Start creating rpm repo')
  332. self.run('createrepo /data/src/', temp_path, 'odoo-rpm-createrepo-%s' % TSTAMP)
  333. shutil.copytree(os.path.join(temp_path, "repodata"), pub_repodata_path)
  334. # Remove temp directory
  335. shutil.rmtree(temp_path)
  336. class DockerWine(Docker):
  337. """Docker class to build Windows package"""
  338. arch = 'win'
  339. def build_image(self):
  340. shutil.copy(os.path.join(self.args.build_dir, 'setup/win32/requirements-local-proxy.txt'), self.docker_dir)
  341. super().build_image()
  342. def build(self):
  343. logging.info('Start building windows package')
  344. winver = "%s.%s" % (VERSION.replace('~', '_').replace('+', ''), TSTAMP)
  345. container_python = '/var/lib/odoo/.wine/drive_c/odoobuild/WinPy64/python-3.12.3.amd64/python.exe'
  346. nsis_args = f'/DVERSION={winver} /DMAJOR_VERSION={version_info[0]} /DMINOR_VERSION={version_info[1]} /DSERVICENAME={nt_service_name} /DPYTHONVERSION=3.12.3'
  347. cmds = [
  348. rf'wine {container_python} -m pip install --upgrade pip',
  349. rf'cat /data/src/requirements*.txt | while read PACKAGE; do wine {container_python} -m pip install "${{PACKAGE%%#*}}" ; done',
  350. rf'wine "c:\nsis-3.10\makensis.exe" {nsis_args} "c:\odoobuild\server\setup\win32\setup.nsi"',
  351. rf'wine {container_python} -m pip list'
  352. ]
  353. self.run(' && '.join(cmds), self.args.build_dir, 'odoo-win-build-%s' % TSTAMP)
  354. logging.info('Finished building Windows package')
  355. def parse_args():
  356. ap = argparse.ArgumentParser()
  357. build_dir = "%s-%s-%s" % (ROOTDIR, TSEC, TSTAMP)
  358. log_levels = {"debug": logging.DEBUG, "info": logging.INFO, "warning": logging.WARN, "error": logging.ERROR, "critical": logging.CRITICAL}
  359. ap.add_argument("-b", "--build-dir", default=build_dir, help="build directory (%(default)s)", metavar="DIR")
  360. ap.add_argument("-p", "--pub", default=None, help="pub directory %(default)s", metavar="DIR")
  361. ap.add_argument("--logging", action="store", choices=list(log_levels.keys()), default="info", help="Logging level")
  362. ap.add_argument("--build-deb", action="store_true")
  363. ap.add_argument("--build-rpm", action="store_true")
  364. ap.add_argument("--build-tgz", action="store_true")
  365. ap.add_argument("--build-win", action="store_true")
  366. ap.add_argument("-t", "--test", action="store_true", default=False, help="Test built packages")
  367. ap.add_argument("-s", "--sign", action="store_true", default=False, help="Sign Debian package / generate Rpm repo")
  368. ap.add_argument("--no-remove", action="store_true", help="don't remove build dir")
  369. ap.add_argument("--blacklist", nargs="*", help="Modules to blacklist in package")
  370. parsed_args = ap.parse_args()
  371. logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %I:%M:%S', level=log_levels[parsed_args.logging])
  372. parsed_args.odoo_dir = ROOTDIR
  373. return parsed_args
  374. def main(args):
  375. try:
  376. if args.build_tgz:
  377. _prepare_build_dir(args)
  378. docker_tgz = DockerTgz(args)
  379. docker_tgz.build()
  380. try:
  381. docker_tgz.start_test()
  382. published_files = publish(args, 'tgz', ['tar.gz', 'zip'])
  383. except Exception as e:
  384. logging.error("Won't publish the tgz release.\n Exception: %s" % str(e))
  385. if args.build_rpm:
  386. _prepare_build_dir(args)
  387. docker_rpm = DockerRpm(args)
  388. docker_rpm.build()
  389. try:
  390. docker_rpm.start_test()
  391. published_files = publish(args, 'rpm', ['rpm'])
  392. if args.sign:
  393. logging.info('Signing rpm package')
  394. rpm_sign(args, published_files[0])
  395. logging.info('Generate rpm repo')
  396. docker_rpm.gen_rpm_repo(args, published_files[0])
  397. except Exception as e:
  398. logging.error("Won't publish the rpm release.\n Exception: %s" % str(e))
  399. if args.build_deb:
  400. _prepare_build_dir(args, move_addons=False)
  401. docker_deb = DockerDeb(args)
  402. docker_deb.build()
  403. try:
  404. docker_deb.start_test()
  405. published_files = publish(args, 'deb', ['deb', 'dsc', 'changes', 'tar.xz'])
  406. gen_deb_package(args, published_files)
  407. except Exception as e:
  408. logging.error("Won't publish the deb release.\n Exception: %s" % str(e))
  409. if args.build_win:
  410. _prepare_build_dir(args, win32=True)
  411. docker_wine = DockerWine(args)
  412. docker_wine.build()
  413. try:
  414. published_files = publish(args, 'windows', ['exe'])
  415. except Exception as e:
  416. logging.error("Won't publish the exe release.\n Exception: %s" % str(e))
  417. except Exception as e:
  418. logging.error('Something bad happened ! : {}'.format(e))
  419. traceback.print_exc()
  420. finally:
  421. if args.no_remove:
  422. logging.info('Build dir "{}" not removed'.format(args.build_dir))
  423. else:
  424. if os.path.exists(args.build_dir):
  425. shutil.rmtree(args.build_dir)
  426. logging.info('Build dir %s removed' % args.build_dir)
  427. if __name__ == '__main__':
  428. args = parse_args()
  429. if os.path.exists(args.build_dir):
  430. logging.error('Build dir "%s" already exists.', args.build_dir)
  431. sys.exit(1)
  432. main(args)
上海开阖软件有限公司 沪ICP备12045867号-1