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.

354 lines
12 KiB

  1. """Doodba child project tasks.
  2. This file is to be executed with https://www.pyinvoke.org/ in Python 3.6+.
  3. Contains common helpers to develop using this child project.
  4. """
  5. import json
  6. import os
  7. from itertools import chain
  8. from pathlib import Path
  9. from shutil import which
  10. from invoke import task
  11. ODOO_VERSION = 14.0
  12. PROJECT_ROOT = Path(__file__).parent.absolute()
  13. SRC_PATH = PROJECT_ROOT / "odoo" / "custom" / "src"
  14. UID_ENV = {"GID": str(os.getgid()), "UID": str(os.getuid()), "UMASK": "27"}
  15. @task
  16. def write_code_workspace_file(c, cw_path=None):
  17. """Generate code-workspace file definition.
  18. Some other tasks will call this one when needed, and since you cannot specify
  19. the file name there, if you want a specific one, you should call this task
  20. before.
  21. Most times you just can forget about this task and let it be run automatically
  22. whenever needed.
  23. If you don't define a workspace name, this task will reuse the 1st
  24. `doodba.*.code-workspace` file found inside the current directory.
  25. If none is found, it will default to `doodba.$(basename $PWD).code-workspace`.
  26. If you define it manually, remember to use the same prefix and suffix if you
  27. want it git-ignored by default.
  28. Example: `--cw-path doodba.my-custom-name.code-workspace`
  29. """
  30. root_name = f"doodba.{PROJECT_ROOT.name}"
  31. root_var = "${workspaceRoot:%s}" % root_name
  32. if not cw_path:
  33. try:
  34. cw_path = next(PROJECT_ROOT.glob("doodba.*.code-workspace"))
  35. except StopIteration:
  36. cw_path = f"{root_name}.code-workspace"
  37. if not Path(cw_path).is_absolute():
  38. cw_path = PROJECT_ROOT / cw_path
  39. cw_config = {}
  40. try:
  41. with open(cw_path) as cw_fd:
  42. cw_config = json.load(cw_fd)
  43. except (FileNotFoundError, json.decoder.JSONDecodeError):
  44. pass # Nevermind, we start with a new config
  45. # Launch configurations
  46. debugpy_configuration = {
  47. "name": "Attach Python debugger to running container",
  48. "type": "python",
  49. "request": "attach",
  50. "pathMappings": [{"localRoot": f"{root_var}/odoo", "remoteRoot": "/opt/odoo"}],
  51. "port": int(ODOO_VERSION) * 1000 + 899,
  52. "host": "localhost",
  53. }
  54. firefox_configuration = {
  55. "type": "firefox",
  56. "request": "launch",
  57. "reAttach": True,
  58. "name": "Connect to firefox debugger",
  59. "url": f"http://localhost:{ODOO_VERSION:.0f}069/?debug=assets",
  60. "reloadOnChange": {
  61. "watch": f"{root_var}/odoo/custom/src/**/*.{'{js,css,scss,less}'}"
  62. },
  63. "skipFiles": ["**/lib/**"],
  64. "pathMappings": [],
  65. }
  66. chrome_executable = which("chrome") or which("chromium")
  67. chrome_configuration = {
  68. "type": "chrome",
  69. "request": "launch",
  70. "name": "Connect to chrome debugger",
  71. "url": f"http://localhost:{ODOO_VERSION:.0f}069/?debug=assets",
  72. "skipFiles": ["**/lib/**"],
  73. "trace": True,
  74. "pathMapping": {},
  75. }
  76. if chrome_executable:
  77. chrome_configuration["runtimeExecutable"] = chrome_executable
  78. cw_config["launch"] = {
  79. "compounds": [
  80. {
  81. "name": "Start Odoo and debug Python",
  82. "configurations": ["Attach Python debugger to running container"],
  83. "preLaunchTask": "Start Odoo in debug mode",
  84. },
  85. {
  86. "name": "Start Odoo and debug JS in Firefox",
  87. "configurations": ["Connect to firefox debugger"],
  88. "preLaunchTask": "Start Odoo",
  89. },
  90. {
  91. "name": "Start Odoo and debug JS in Chrome",
  92. "configurations": ["Connect to chrome debugger"],
  93. "preLaunchTask": "Start Odoo",
  94. },
  95. {
  96. "name": "Start Odoo and debug Python + JS in Firefox",
  97. "configurations": [
  98. "Attach Python debugger to running container",
  99. "Connect to firefox debugger",
  100. ],
  101. "preLaunchTask": "Start Odoo in debug mode",
  102. },
  103. {
  104. "name": "Start Odoo and debug Python + JS in Chrome",
  105. "configurations": [
  106. "Attach Python debugger to running container",
  107. "Connect to chrome debugger",
  108. ],
  109. "preLaunchTask": "Start Odoo in debug mode",
  110. },
  111. ],
  112. "configurations": [
  113. debugpy_configuration,
  114. firefox_configuration,
  115. chrome_configuration,
  116. ],
  117. }
  118. # Configure folders
  119. cw_config["folders"] = []
  120. for subrepo in SRC_PATH.glob("*"):
  121. if not subrepo.is_dir():
  122. continue
  123. if (subrepo / ".git").exists() and subrepo.name != "odoo":
  124. cw_config["folders"].append(
  125. {"path": str(subrepo.relative_to(PROJECT_ROOT))}
  126. )
  127. debugpy_configuration["pathMappings"].append(
  128. {
  129. "localRoot": "${workspaceRoot:%s}" % subrepo.name,
  130. "remoteRoot": f"/opt/odoo/custom/src/{subrepo.name}",
  131. }
  132. )
  133. for addon in chain(subrepo.glob("*"), subrepo.glob("addons/*")):
  134. if (addon / "__manifest__.py").is_file() or (
  135. addon / "__openerp__.py"
  136. ).is_file():
  137. url = f"http://localhost:{ODOO_VERSION:.0f}069/{addon.name}/static/"
  138. path = "${{workspaceRoot:{}}}/{}/static/".format(
  139. subrepo.name,
  140. addon.relative_to(subrepo),
  141. )
  142. firefox_configuration["pathMappings"].append({"url": url, "path": path})
  143. chrome_configuration["pathMapping"][url] = path
  144. cw_config["tasks"] = {
  145. "version": "2.0.0",
  146. "tasks": [
  147. {
  148. "label": "Start Odoo",
  149. "type": "process",
  150. "command": "invoke",
  151. "args": ["start", "--detach"],
  152. "presentation": {
  153. "echo": True,
  154. "reveal": "silent",
  155. "focus": False,
  156. "panel": "shared",
  157. "showReuseMessage": True,
  158. "clear": False,
  159. },
  160. "problemMatcher": [],
  161. },
  162. {
  163. "label": "Start Odoo in debug mode",
  164. "type": "process",
  165. "command": "invoke",
  166. "args": ["start", "--detach", "--debugpy"],
  167. "presentation": {
  168. "echo": True,
  169. "reveal": "silent",
  170. "focus": False,
  171. "panel": "shared",
  172. "showReuseMessage": True,
  173. "clear": False,
  174. },
  175. "problemMatcher": [],
  176. },
  177. {
  178. "label": "Stop Odoo",
  179. "type": "process",
  180. "command": "invoke",
  181. "args": ["stop"],
  182. "presentation": {
  183. "echo": True,
  184. "reveal": "silent",
  185. "focus": False,
  186. "panel": "shared",
  187. "showReuseMessage": True,
  188. "clear": False,
  189. },
  190. "problemMatcher": [],
  191. },
  192. ],
  193. }
  194. # Sort project folders
  195. cw_config["folders"].sort(key=lambda x: x["path"])
  196. # Put Odoo folder just before private and top folder
  197. odoo = SRC_PATH / "odoo"
  198. if odoo.is_dir():
  199. cw_config["folders"].append({"path": str(odoo.relative_to(PROJECT_ROOT))})
  200. # HACK https://github.com/microsoft/vscode/issues/95963 put private second to last
  201. private = SRC_PATH / "private"
  202. if private.is_dir():
  203. cw_config["folders"].append({"path": str(private.relative_to(PROJECT_ROOT))})
  204. # HACK https://github.com/microsoft/vscode/issues/37947 put top folder last
  205. cw_config["folders"].append({"path": ".", "name": root_name})
  206. with open(cw_path, "w") as cw_fd:
  207. json.dump(cw_config, cw_fd, indent=2)
  208. cw_fd.write("\n")
  209. @task
  210. def develop(c):
  211. """Set up a basic development environment."""
  212. # Prepare environment
  213. Path(PROJECT_ROOT, "odoo", "auto", "addons").mkdir(parents=True, exist_ok=True)
  214. with c.cd(str(PROJECT_ROOT)):
  215. c.run("git init")
  216. c.run("ln -sf devel.yaml docker-compose.yml")
  217. write_code_workspace_file(c)
  218. c.run("pre-commit install")
  219. @task(develop)
  220. def git_aggregate(c):
  221. """Download odoo & addons git code.
  222. Executes git-aggregator from within the doodba container.
  223. """
  224. with c.cd(str(PROJECT_ROOT)):
  225. c.run(
  226. "docker-compose --file setup-devel.yaml run --rm odoo",
  227. env=UID_ENV,
  228. )
  229. write_code_workspace_file(c)
  230. for git_folder in SRC_PATH.glob("*/.git/.."):
  231. action = (
  232. "install"
  233. if (git_folder / ".pre-commit-config.yaml").is_file()
  234. else "uninstall"
  235. )
  236. with c.cd(str(git_folder)):
  237. c.run(f"pre-commit {action}")
  238. @task(develop)
  239. def img_build(c, pull=True):
  240. """Build docker images."""
  241. cmd = "docker-compose build"
  242. if pull:
  243. cmd += " --pull"
  244. with c.cd(str(PROJECT_ROOT)):
  245. c.run(cmd, env=UID_ENV)
  246. @task(develop)
  247. def img_pull(c):
  248. """Pull docker images."""
  249. with c.cd(str(PROJECT_ROOT)):
  250. c.run("docker-compose pull")
  251. @task(develop)
  252. def lint(c, verbose=False):
  253. """Lint & format source code."""
  254. cmd = "pre-commit run --show-diff-on-failure --all-files --color=always"
  255. if verbose:
  256. cmd += " --verbose"
  257. with c.cd(str(PROJECT_ROOT)):
  258. c.run(cmd)
  259. @task(develop)
  260. def start(c, detach=True, debugpy=False):
  261. """Start environment."""
  262. cmd = "docker-compose up"
  263. if detach:
  264. cmd += " --detach"
  265. with c.cd(str(PROJECT_ROOT)):
  266. c.run(cmd, env=dict(UID_ENV, DOODBA_DEBUGPY_ENABLE=str(int(debugpy))))
  267. @task(
  268. develop,
  269. help={"purge": "Remove all related containers, networks images and volumes"},
  270. )
  271. def stop(c, purge=False):
  272. """Stop and (optionally) purge environment."""
  273. cmd = "docker-compose"
  274. if purge:
  275. cmd += " down --remove-orphans --rmi local --volumes"
  276. else:
  277. cmd += " stop"
  278. with c.cd(str(PROJECT_ROOT)):
  279. c.run(cmd)
  280. @task(
  281. develop,
  282. help={
  283. "dbname": "The DB that will be DESTROYED and recreated. Default: 'devel'.",
  284. "modules": "Comma-separated list of modules to install. Default: 'base'.",
  285. },
  286. )
  287. def resetdb(c, modules="base", dbname="devel"):
  288. """Reset the specified database with the specified modules.
  289. Uses click-odoo-initdb behind the scenes, which has a caching system that
  290. makes DB resets quicker. See its docs for more info.
  291. """
  292. with c.cd(str(PROJECT_ROOT)):
  293. c.run("docker-compose stop odoo", pty=True)
  294. _run = "docker-compose run --rm -l traefik.enable=false odoo"
  295. c.run(
  296. f"{_run} click-odoo-dropdb {dbname}",
  297. env=UID_ENV,
  298. warn=True,
  299. pty=True,
  300. )
  301. c.run(
  302. f"{_run} click-odoo-initdb -n {dbname} -m {modules}",
  303. env=UID_ENV,
  304. pty=True,
  305. )
  306. @task(develop)
  307. def restart(c, quick=True):
  308. """Restart odoo container(s)."""
  309. cmd = "docker-compose restart"
  310. if quick:
  311. cmd = f"{cmd} -t0"
  312. cmd = f"{cmd} odoo odoo_proxy"
  313. with c.cd(str(PROJECT_ROOT)):
  314. c.run(cmd, env=UID_ENV)
  315. @task(develop)
  316. def logs(c, tail=10):
  317. """Obtain last logs of current environment."""
  318. cmd = "docker-compose logs -f"
  319. if tail:
  320. cmd += f" --tail {tail}"
  321. with c.cd(str(PROJECT_ROOT)):
  322. c.run(cmd)