go-vendor.bbclass 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. #
  2. # Copyright 2023 (C) Weidmueller GmbH & Co KG
  3. # Author: Lukas Funke <lukas.funke@weidmueller.com>
  4. #
  5. # Handle Go vendor support for offline builds
  6. #
  7. # When importing Go modules, Go downloads the imported modules using
  8. # a network (proxy) connection ahead of the compile stage. This contradicts
  9. # the yocto build concept of fetching every source ahead of build-time
  10. # and supporting offline builds.
  11. #
  12. # To support offline builds, we use Go 'vendoring': module dependencies are
  13. # downloaded during the fetch-phase and unpacked into the modules 'vendor'
  14. # folder. Additionally a manifest file is generated for the 'vendor' folder
  15. #
  16. inherit go-mod
  17. def go_src_uri(repo, version, path=None, subdir=None, \
  18. vcs='git', replaces=None, pathmajor=None):
  19. destsuffix = "git/src/import/vendor.fetch"
  20. module_path = repo if not path else path
  21. src_uri = "{}://{};name={}".format(vcs, repo, module_path.replace('/', '.'))
  22. src_uri += ";destsuffix={}/{}@{}".format(destsuffix, repo, version)
  23. if vcs == "git":
  24. src_uri += ";nobranch=1;protocol=https"
  25. src_uri += ";go_module_path={}".format(module_path)
  26. if replaces:
  27. src_uri += ";go_module_replacement={}".format(replaces)
  28. if subdir:
  29. src_uri += ";go_subdir={}".format(subdir)
  30. if pathmajor:
  31. src_uri += ";go_pathmajor={}".format(pathmajor)
  32. src_uri += ";is_go_dependency=1"
  33. return src_uri
  34. python do_vendor_unlink() {
  35. go_import = d.getVar('GO_IMPORT')
  36. source_dir = d.getVar('S')
  37. linkname = os.path.join(source_dir, *['src', go_import, 'vendor'])
  38. os.unlink(linkname)
  39. }
  40. addtask vendor_unlink before do_package after do_install
  41. python do_go_vendor() {
  42. import shutil
  43. src_uri = (d.getVar('SRC_URI') or "").split()
  44. if not src_uri:
  45. bb.fatal("SRC_URI is empty")
  46. default_destsuffix = "git/src/import/vendor.fetch"
  47. fetcher = bb.fetch2.Fetch(src_uri, d)
  48. go_import = d.getVar('GO_IMPORT')
  49. source_dir = d.getVar('S')
  50. linkname = os.path.join(source_dir, *['src', go_import, 'vendor'])
  51. vendor_dir = os.path.join(source_dir, *['src', 'import', 'vendor'])
  52. import_dir = os.path.join(source_dir, *['src', 'import', 'vendor.fetch'])
  53. if os.path.exists(vendor_dir):
  54. # Nothing to do except re-establish link to actual vendor folder
  55. if not os.path.exists(linkname):
  56. oe.path.relsymlink(vendor_dir, linkname)
  57. return
  58. bb.utils.mkdirhier(vendor_dir)
  59. modules = {}
  60. for url in fetcher.urls:
  61. srcuri = fetcher.ud[url].host + fetcher.ud[url].path
  62. # Skip non Go module src uris
  63. if not fetcher.ud[url].parm.get('is_go_dependency'):
  64. continue
  65. destsuffix = fetcher.ud[url].parm.get('destsuffix')
  66. # We derive the module repo / version in the following manner (exmaple):
  67. #
  68. # destsuffix = git/src/import/vendor.fetch/github.com/foo/bar@v1.2.3
  69. # p = github.com/foo/bar@v1.2.3
  70. # repo = github.com/foo/bar
  71. # version = v1.2.3
  72. p = destsuffix[len(default_destsuffix)+1:]
  73. repo, version = p.split('@')
  74. module_path = fetcher.ud[url].parm.get('go_module_path')
  75. subdir = fetcher.ud[url].parm.get('go_subdir')
  76. subdir = None if not subdir else subdir
  77. pathMajor = fetcher.ud[url].parm.get('go_pathmajor')
  78. pathMajor = None if not pathMajor else pathMajor.strip('/')
  79. if not (repo, version) in modules:
  80. modules[(repo, version)] = {
  81. "repo_path": os.path.join(import_dir, p),
  82. "module_path": module_path,
  83. "subdir": subdir,
  84. "pathMajor": pathMajor }
  85. for module_key, module in modules.items():
  86. # only take the version which is explicitly listed
  87. # as a dependency in the go.mod
  88. module_path = module['module_path']
  89. rootdir = module['repo_path']
  90. subdir = module['subdir']
  91. pathMajor = module['pathMajor']
  92. src = rootdir
  93. if subdir:
  94. src = os.path.join(rootdir, subdir)
  95. # If the module is released at major version 2 or higher, the module
  96. # path must end with a major version suffix like /v2.
  97. # This may or may not be part of the subdirectory name
  98. #
  99. # https://go.dev/ref/mod#modules-overview
  100. if pathMajor:
  101. tmp = os.path.join(src, pathMajor)
  102. # source directory including major version path may or may not exist
  103. if os.path.exists(tmp):
  104. src = tmp
  105. dst = os.path.join(vendor_dir, module_path)
  106. bb.debug(1, "cp %s --> %s" % (src, dst))
  107. shutil.copytree(src, dst, symlinks=True, dirs_exist_ok=True, \
  108. ignore=shutil.ignore_patterns(".git", \
  109. "vendor", \
  110. "*._test.go"))
  111. # If the root directory has a LICENSE file but not the subdir
  112. # we copy the root license to the sub module since the license
  113. # applies to all modules in the repository
  114. # see https://go.dev/ref/mod#vcs-license
  115. if subdir:
  116. rootdirLicese = os.path.join(rootdir, "LICENSE")
  117. subdirLicense = os.path.join(src, "LICENSE")
  118. if not os.path.exists(subdir) and \
  119. os.path.exists(rootdirLicese):
  120. shutil.copy2(rootdirLicese, subdirLicense)
  121. # Copy vendor manifest
  122. modules_txt_src = os.path.join(d.getVar('UNPACKDIR'), "modules.txt")
  123. bb.debug(1, "cp %s --> %s" % (modules_txt_src, vendor_dir))
  124. shutil.copy2(modules_txt_src, vendor_dir)
  125. # Clean up vendor dir
  126. # We only require the modules in the modules_txt file
  127. fetched_paths = set([os.path.relpath(x[0], vendor_dir) for x in os.walk(vendor_dir)])
  128. # Remove toplevel dir
  129. fetched_paths.remove('.')
  130. vendored_paths = set()
  131. replaced_paths = dict()
  132. with open(modules_txt_src) as f:
  133. for line in f:
  134. if not line.startswith("#"):
  135. line = line.strip()
  136. vendored_paths.add(line)
  137. # Add toplevel dirs into vendored dir, as we want to keep them
  138. topdir = os.path.dirname(line)
  139. while len(topdir):
  140. if not topdir in vendored_paths:
  141. vendored_paths.add(topdir)
  142. topdir = os.path.dirname(topdir)
  143. else:
  144. replaced_module = line.split("=>")
  145. if len(replaced_module) > 1:
  146. # This module has been replaced, use a local path
  147. # we parse the line that has a pattern "# module-name [module-version] => local-path
  148. actual_path = replaced_module[1].strip()
  149. vendored_name = replaced_module[0].split()[1]
  150. bb.debug(1, "added vendored name %s for actual path %s" % (vendored_name, actual_path))
  151. replaced_paths[vendored_name] = actual_path
  152. for path in fetched_paths:
  153. if path not in vendored_paths:
  154. realpath = os.path.join(vendor_dir, path)
  155. if os.path.exists(realpath):
  156. shutil.rmtree(realpath)
  157. for vendored_name, replaced_path in replaced_paths.items():
  158. symlink_target = os.path.join(source_dir, *['src', go_import, replaced_path])
  159. symlink_name = os.path.join(vendor_dir, vendored_name)
  160. relative_symlink_target = os.path.relpath(symlink_target, os.path.dirname(symlink_name))
  161. bb.debug(1, "vendored name %s, symlink name %s" % (vendored_name, symlink_name))
  162. os.makedirs(os.path.dirname(symlink_name), exist_ok=True)
  163. os.symlink(relative_symlink_target, symlink_name)
  164. # Create a symlink to the actual directory
  165. relative_vendor_dir = os.path.relpath(vendor_dir, os.path.dirname(linkname))
  166. os.symlink(relative_vendor_dir, linkname)
  167. }
  168. addtask go_vendor before do_patch after do_unpack