misc.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. # Copyright (C) 2012 Anaconda, Inc
  2. # SPDX-License-Identifier: BSD-3-Clause
  3. # this module contains miscellaneous stuff which eventually could be moved
  4. # into other places
  5. import os
  6. import re
  7. import shutil
  8. import sys
  9. from collections import defaultdict
  10. from os.path import abspath, dirname, exists, isdir, isfile, join, relpath
  11. from .base.context import context
  12. from .common.compat import on_win, open
  13. from .common.path import expand
  14. from .common.url import is_url, join_url, path_to_url
  15. from .core.index import get_index
  16. from .core.link import PrefixSetup, UnlinkLinkTransaction
  17. from .core.package_cache_data import PackageCacheData, ProgressiveFetchExtract
  18. from .core.prefix_data import PrefixData
  19. from .exceptions import (
  20. CondaExitZero,
  21. DisallowedPackageError,
  22. DryRunExit,
  23. PackagesNotFoundError,
  24. ParseError,
  25. )
  26. from .gateways.disk.delete import rm_rf
  27. from .gateways.disk.link import islink, readlink, symlink
  28. from .models.match_spec import MatchSpec
  29. from .models.prefix_graph import PrefixGraph
  30. from .plan import _get_best_prec_match
  31. def conda_installed_files(prefix, exclude_self_build=False):
  32. """
  33. Return the set of files which have been installed (using conda) into
  34. a given prefix.
  35. """
  36. res = set()
  37. for meta in PrefixData(prefix).iter_records():
  38. if exclude_self_build and "file_hash" in meta:
  39. continue
  40. res.update(set(meta.get("files", ())))
  41. return res
  42. url_pat = re.compile(
  43. r"(?:(?P<url_p>.+)(?:[/\\]))?"
  44. r"(?P<fn>[^/\\#]+(?:\.tar\.bz2|\.conda))"
  45. r"(:?#(?P<md5>[0-9a-f]{32}))?$"
  46. )
  47. def explicit(
  48. specs, prefix, verbose=False, force_extract=True, index_args=None, index=None
  49. ):
  50. actions = defaultdict(list)
  51. actions["PREFIX"] = prefix
  52. fetch_specs = []
  53. for spec in specs:
  54. if spec == "@EXPLICIT":
  55. continue
  56. if not is_url(spec):
  57. """
  58. # This does not work because url_to_path does not enforce Windows
  59. # backslashes. Should it? Seems like a dangerous change to make but
  60. # it would be cleaner.
  61. expanded = expand(spec)
  62. urled = path_to_url(expanded)
  63. pathed = url_to_path(urled)
  64. assert pathed == expanded
  65. """
  66. spec = path_to_url(expand(spec))
  67. # parse URL
  68. m = url_pat.match(spec)
  69. if m is None:
  70. raise ParseError("Could not parse explicit URL: %s" % spec)
  71. url_p, fn, md5sum = m.group("url_p"), m.group("fn"), m.group("md5")
  72. url = join_url(url_p, fn)
  73. # url_p is everything but the tarball_basename and the md5sum
  74. fetch_specs.append(MatchSpec(url, md5=md5sum) if md5sum else MatchSpec(url))
  75. if context.dry_run:
  76. raise DryRunExit()
  77. pfe = ProgressiveFetchExtract(fetch_specs)
  78. pfe.execute()
  79. if context.download_only:
  80. raise CondaExitZero(
  81. "Package caches prepared. "
  82. "UnlinkLinkTransaction cancelled with --download-only option."
  83. )
  84. # now make an UnlinkLinkTransaction with the PackageCacheRecords as inputs
  85. # need to add package name to fetch_specs so that history parsing keeps track of them correctly
  86. specs_pcrecs = tuple(
  87. [spec, next(PackageCacheData.query_all(spec), None)] for spec in fetch_specs
  88. )
  89. # Assert that every spec has a PackageCacheRecord
  90. specs_with_missing_pcrecs = [
  91. str(spec) for spec, pcrec in specs_pcrecs if pcrec is None
  92. ]
  93. if specs_with_missing_pcrecs:
  94. if len(specs_with_missing_pcrecs) == len(specs_pcrecs):
  95. raise AssertionError("No package cache records found")
  96. else:
  97. missing_precs_list = ", ".join(specs_with_missing_pcrecs)
  98. raise AssertionError(
  99. f"Missing package cache records for: {missing_precs_list}"
  100. )
  101. precs_to_remove = []
  102. prefix_data = PrefixData(prefix)
  103. for q, (spec, pcrec) in enumerate(specs_pcrecs):
  104. new_spec = MatchSpec(spec, name=pcrec.name)
  105. specs_pcrecs[q][0] = new_spec
  106. prec = prefix_data.get(pcrec.name, None)
  107. if prec:
  108. # If we've already got matching specifications, then don't bother re-linking it
  109. if next(prefix_data.query(new_spec), None):
  110. specs_pcrecs[q][0] = None
  111. else:
  112. precs_to_remove.append(prec)
  113. stp = PrefixSetup(
  114. prefix,
  115. precs_to_remove,
  116. tuple(sp[1] for sp in specs_pcrecs if sp[0]),
  117. (),
  118. tuple(sp[0] for sp in specs_pcrecs if sp[0]),
  119. (),
  120. )
  121. txn = UnlinkLinkTransaction(stp)
  122. txn.execute()
  123. def rel_path(prefix, path, windows_forward_slashes=True):
  124. res = path[len(prefix) + 1 :]
  125. if on_win and windows_forward_slashes:
  126. res = res.replace("\\", "/")
  127. return res
  128. def walk_prefix(prefix, ignore_predefined_files=True, windows_forward_slashes=True):
  129. """Return the set of all files in a given prefix directory."""
  130. res = set()
  131. prefix = abspath(prefix)
  132. ignore = {
  133. "pkgs",
  134. "envs",
  135. "conda-bld",
  136. "conda-meta",
  137. ".conda_lock",
  138. "users",
  139. "LICENSE.txt",
  140. "info",
  141. "conda-recipes",
  142. ".index",
  143. ".unionfs",
  144. ".nonadmin",
  145. }
  146. binignore = {"conda", "activate", "deactivate"}
  147. if sys.platform == "darwin":
  148. ignore.update({"python.app", "Launcher.app"})
  149. for fn in (entry.name for entry in os.scandir(prefix)):
  150. if ignore_predefined_files and fn in ignore:
  151. continue
  152. if isfile(join(prefix, fn)):
  153. res.add(fn)
  154. continue
  155. for root, dirs, files in os.walk(join(prefix, fn)):
  156. should_ignore = ignore_predefined_files and root == join(prefix, "bin")
  157. for fn2 in files:
  158. if should_ignore and fn2 in binignore:
  159. continue
  160. res.add(relpath(join(root, fn2), prefix))
  161. for dn in dirs:
  162. path = join(root, dn)
  163. if islink(path):
  164. res.add(relpath(path, prefix))
  165. if on_win and windows_forward_slashes:
  166. return {path.replace("\\", "/") for path in res}
  167. else:
  168. return res
  169. def untracked(prefix, exclude_self_build=False):
  170. """Return (the set) of all untracked files for a given prefix."""
  171. conda_files = conda_installed_files(prefix, exclude_self_build)
  172. return {
  173. path
  174. for path in walk_prefix(prefix) - conda_files
  175. if not (
  176. path.endswith("~")
  177. or sys.platform == "darwin"
  178. and path.endswith(".DS_Store")
  179. or path.endswith(".pyc")
  180. and path[:-1] in conda_files
  181. )
  182. }
  183. def touch_nonadmin(prefix):
  184. """Creates $PREFIX/.nonadmin if sys.prefix/.nonadmin exists (on Windows)."""
  185. if on_win and exists(join(context.root_prefix, ".nonadmin")):
  186. if not isdir(prefix):
  187. os.makedirs(prefix)
  188. with open(join(prefix, ".nonadmin"), "w") as fo:
  189. fo.write("")
  190. def clone_env(prefix1, prefix2, verbose=True, quiet=False, index_args=None):
  191. """Clone existing prefix1 into new prefix2."""
  192. untracked_files = untracked(prefix1)
  193. # Discard conda, conda-env and any package that depends on them
  194. filter = {}
  195. found = True
  196. while found:
  197. found = False
  198. for prec in PrefixData(prefix1).iter_records():
  199. name = prec["name"]
  200. if name in filter:
  201. continue
  202. if name == "conda":
  203. filter["conda"] = prec
  204. found = True
  205. break
  206. if name == "conda-env":
  207. filter["conda-env"] = prec
  208. found = True
  209. break
  210. for dep in prec.combined_depends:
  211. if MatchSpec(dep).name in filter:
  212. filter[name] = prec
  213. found = True
  214. if filter:
  215. if not quiet:
  216. fh = sys.stderr if context.json else sys.stdout
  217. print(
  218. "The following packages cannot be cloned out of the root environment:",
  219. file=fh,
  220. )
  221. for prec in filter.values():
  222. print(" - " + prec.dist_str(), file=fh)
  223. drecs = {
  224. prec
  225. for prec in PrefixData(prefix1).iter_records()
  226. if prec["name"] not in filter
  227. }
  228. else:
  229. drecs = {prec for prec in PrefixData(prefix1).iter_records()}
  230. # Resolve URLs for packages that do not have URLs
  231. index = {}
  232. unknowns = [prec for prec in drecs if not prec.get("url")]
  233. notfound = []
  234. if unknowns:
  235. index_args = index_args or {}
  236. index = get_index(**index_args)
  237. for prec in unknowns:
  238. spec = MatchSpec(name=prec.name, version=prec.version, build=prec.build)
  239. precs = tuple(prec for prec in index.values() if spec.match(prec))
  240. if not precs:
  241. notfound.append(spec)
  242. elif len(precs) > 1:
  243. drecs.remove(prec)
  244. drecs.add(_get_best_prec_match(precs))
  245. else:
  246. drecs.remove(prec)
  247. drecs.add(precs[0])
  248. if notfound:
  249. raise PackagesNotFoundError(notfound)
  250. # Assemble the URL and channel list
  251. urls = {}
  252. for prec in drecs:
  253. urls[prec] = prec["url"]
  254. precs = tuple(PrefixGraph(urls).graph)
  255. urls = [urls[prec] for prec in precs]
  256. disallowed = tuple(MatchSpec(s) for s in context.disallowed_packages)
  257. for prec in precs:
  258. if any(d.match(prec) for d in disallowed):
  259. raise DisallowedPackageError(prec)
  260. if verbose:
  261. print("Packages: %d" % len(precs))
  262. print("Files: %d" % len(untracked_files))
  263. if context.dry_run:
  264. raise DryRunExit()
  265. for f in untracked_files:
  266. src = join(prefix1, f)
  267. dst = join(prefix2, f)
  268. dst_dir = dirname(dst)
  269. if islink(dst_dir) or isfile(dst_dir):
  270. rm_rf(dst_dir)
  271. if not isdir(dst_dir):
  272. os.makedirs(dst_dir)
  273. if islink(src):
  274. symlink(readlink(src), dst)
  275. continue
  276. try:
  277. with open(src, "rb") as fi:
  278. data = fi.read()
  279. except OSError:
  280. continue
  281. try:
  282. s = data.decode("utf-8")
  283. s = s.replace(prefix1, prefix2)
  284. data = s.encode("utf-8")
  285. except UnicodeDecodeError: # data is binary
  286. pass
  287. with open(dst, "wb") as fo:
  288. fo.write(data)
  289. shutil.copystat(src, dst)
  290. actions = explicit(
  291. urls,
  292. prefix2,
  293. verbose=not quiet,
  294. index=index,
  295. force_extract=False,
  296. index_args=index_args,
  297. )
  298. return actions, untracked_files