Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/metadata2 #14152

Merged
merged 7 commits into from
Jul 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion conan/api/subapi/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,5 @@ def package(self, pref: PkgReference, remote: Remote, metadata=None):
return False

output.info(f"Downloading package '{pref.repr_notime()}'")
app.remote_manager.get_package(pref, remote)
app.remote_manager.get_package(pref, remote, metadata)
return True
4 changes: 3 additions & 1 deletion conans/client/cmd/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ def cmd_export(app, conanfile_path, name, version, user, channel, graph_lock=Non
# TODO: cache2.0 move this creation to other place
mkdir(export_folder)
mkdir(export_src_folder)
conanfile.folders.set_base_recipe_metadata(recipe_layout.metadata())
recipe_metadata = recipe_layout.metadata()
mkdir(recipe_metadata)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automatic creation of the folder, so conan cache path --folder=metadata has an existing folder to copy things to.

conanfile.folders.set_base_recipe_metadata(recipe_metadata)
export_recipe(conanfile, export_folder)
export_source(conanfile, export_src_folder)
shutil.copy2(conanfile_path, recipe_layout.conanfile())
Expand Down
1 change: 1 addition & 0 deletions conans/client/conanfile/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

def run_build_method(conanfile, hook_manager):
mkdir(conanfile.build_folder)
mkdir(conanfile.package_metadata_folder)
with chdir(conanfile.build_folder):
hook_manager.execute("pre_build", conanfile=conanfile)
if hasattr(conanfile, "build"):
Expand Down
6 changes: 3 additions & 3 deletions conans/client/downloaders/caching_file_downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,10 @@ def __init__(self, requester, config, scope=None):
raise ConanException("core.download:download_cache must be an absolute path")
self._file_downloader = FileDownloader(requester, scope=scope)

def download(self, url, file_path, auth, verify_ssl, retry, retry_wait):
if not self._download_cache:
def download(self, url, file_path, auth, verify_ssl, retry, retry_wait, metadata=False):
if not self._download_cache or metadata: # Metadata not cached and can be overwritten
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The metadata cannot be cached, as it is not immutable, and not controlled by checksum

self._file_downloader.download(url, file_path, retry=retry, retry_wait=retry_wait,
verify_ssl=verify_ssl, auth=auth, overwrite=False)
verify_ssl=verify_ssl, auth=auth, overwrite=metadata)
return

download_cache = DownloadCache(self._download_cache)
Expand Down
4 changes: 3 additions & 1 deletion conans/client/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def build_package(self, node, package_layout):
prev = self._package(conanfile, pref)
assert prev
node.prev = prev
except ConanException as exc:
except ConanException as exc: # TODO: Remove this? unnecessary?
raise exc

return node.pref
Expand Down Expand Up @@ -326,12 +326,14 @@ def _handle_package(self, package, install_reference, handled_count, total_count

# Make sure that all nodes with same pref compute package_info()
pkg_folder = package_layout.package()
pkg_metadata = package_layout.metadata()
assert os.path.isdir(pkg_folder), "Pkg '%s' folder must exist: %s" % (str(pref), pkg_folder)
for n in package.nodes:
n.prev = pref.revision # Make sure the prev is assigned
conanfile = n.conanfile
# Call the info method
conanfile.folders.set_base_package(pkg_folder)
conanfile.folders.set_base_pkg_metadata(pkg_metadata)
Copy link
Member Author

@memsharded memsharded Jun 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows deployers to access dep.package_metadata_folder to collect and deploy metadata.

self._call_package_info(conanfile, pkg_folder, is_editable=False)

def _handle_node_editable(self, install_node):
Expand Down
73 changes: 42 additions & 31 deletions conans/client/rest/rest_client_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,24 @@ def _get_file_list_json(self, url):
def get_recipe(self, ref, dest_folder, metadata, only_metadata):
url = self.router.recipe_snapshot(ref)
data = self._get_file_list_json(url)
files = data["files"]
accepted_files = ["conanfile.py", "conan_export.tgz", "conanmanifest.txt", "metadata/sign"]
if only_metadata:
accepted_files = []
metadata = metadata or []
metadata = [f"metadata/{m}" for m in metadata]
files = [f for f in files if any(f.startswith(m) for m in accepted_files)
or any(fnmatch.fnmatch(f, m) for m in metadata)]

# If we didn't indicated reference, server got the latest, use absolute now, it's safer
urls = {fn: self.router.recipe_file(ref, fn) for fn in files}
self._download_and_save_files(urls, dest_folder, files, parallel=True)
ret = {fn: os.path.join(dest_folder, fn) for fn in files}
return ret
server_files = data["files"]
result = {}

if not only_metadata:
accepted_files = ["conanfile.py", "conan_export.tgz", "conanmanifest.txt",
"metadata/sign"]
files = [f for f in server_files if any(f.startswith(m) for m in accepted_files)]
# If we didn't indicated reference, server got the latest, use absolute now, it's safer
urls = {fn: self.router.recipe_file(ref, fn) for fn in files}
self._download_and_save_files(urls, dest_folder, files, parallel=True)
result.update({fn: os.path.join(dest_folder, fn) for fn in files})
if metadata:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to separate the download to define metadata=True that allows 1) not caching and 2) allows overwriting existing files

metadata = [f"metadata/{m}" for m in metadata]
files = [f for f in server_files if any(fnmatch.fnmatch(f, m) for m in metadata)]
urls = {fn: self.router.recipe_file(ref, fn) for fn in files}
self._download_and_save_files(urls, dest_folder, files, parallel=True, metadata=True)
result.update({fn: os.path.join(dest_folder, fn) for fn in files})
return result

def get_recipe_sources(self, ref, dest_folder):
# If revision not specified, check latest
Expand All @@ -73,21 +77,26 @@ def get_recipe_sources(self, ref, dest_folder):
def get_package(self, pref, dest_folder, metadata, only_metadata):
url = self.router.package_snapshot(pref)
data = self._get_file_list_json(url)
files = data["files"]
server_files = data["files"]
result = {}
# Download only known files, but not metadata (except sign)
accepted_files = ["conaninfo.txt", "conan_package.tgz", "conanmanifest.txt", "metadata/sign"]
if only_metadata:
accepted_files = []
metadata = metadata or []
metadata = [f"metadata/{m}" for m in metadata]
files = [f for f in files if any(f.startswith(m) for m in accepted_files)
or any(fnmatch.fnmatch(f, m) for m in metadata)]

# If we didn't indicated reference, server got the latest, use absolute now, it's safer
urls = {fn: self.router.package_file(pref, fn) for fn in files}
self._download_and_save_files(urls, dest_folder, files, scope=str(pref.ref))
ret = {fn: os.path.join(dest_folder, fn) for fn in files}
return ret
if not only_metadata: # Retrieve package first, then metadata
accepted_files = ["conaninfo.txt", "conan_package.tgz", "conanmanifest.txt",
"metadata/sign"]
files = [f for f in server_files if any(f.startswith(m) for m in accepted_files)]
# If we didn't indicated reference, server got the latest, use absolute now, it's safer
urls = {fn: self.router.package_file(pref, fn) for fn in files}
self._download_and_save_files(urls, dest_folder, files, scope=str(pref.ref))
result.update({fn: os.path.join(dest_folder, fn) for fn in files})

if metadata:
metadata = [f"metadata/{m}" for m in metadata]
files = [f for f in server_files if any(fnmatch.fnmatch(f, m) for m in metadata)]
urls = {fn: self.router.package_file(pref, fn) for fn in files}
self._download_and_save_files(urls, dest_folder, files, scope=str(pref.ref),
metadata=True)
result.update({fn: os.path.join(dest_folder, fn) for fn in files})
return result

@staticmethod
def _is_dir(path, files):
Expand Down Expand Up @@ -145,7 +154,8 @@ def _upload_files(self, files, urls):
raise ConanException("Execute upload again to retry upload the failed files: %s"
% ", ".join(failed))

def _download_and_save_files(self, urls, dest_folder, files, parallel=False, scope=None):
def _download_and_save_files(self, urls, dest_folder, files, parallel=False, scope=None,
metadata=False):
# Take advantage of filenames ordering, so that conan_package.tgz and conan_export.tgz
# can be < conanfile, conaninfo, and sent always the last, so smaller files go first
retry = self._config.get("core.download:retry", check_type=int, default=2)
Expand All @@ -160,13 +170,14 @@ def _download_and_save_files(self, urls, dest_folder, files, parallel=False, sco
if parallel:
kwargs = {"url": resource_url, "file_path": abs_path, "retry": retry,
"retry_wait": retry_wait, "verify_ssl": self.verify_ssl,
"auth": self.auth}
"auth": self.auth, "metadata": metadata}
thread = ExceptionThread(target=downloader.download, kwargs=kwargs)
threads.append(thread)
thread.start()
else:
downloader.download(url=resource_url, file_path=abs_path, auth=self.auth,
verify_ssl=self.verify_ssl, retry=retry, retry_wait=retry_wait)
verify_ssl=self.verify_ssl, retry=retry, retry_wait=retry_wait,
metadata=metadata)
for t in threads:
t.join()
for t in threads: # Need to join all before raising errors
Expand Down
1 change: 1 addition & 0 deletions conans/client/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def config_source(export_source_folder, conanfile, hook_manager):
if not os.path.exists(conanfile.folders.base_source): # No source folder, need to get it
with set_dirty_context_manager(conanfile.folders.base_source):
mkdir(conanfile.source_folder)
mkdir(conanfile.recipe_metadata_folder)

# First of all get the exported scm sources (if auto) or clone (if fixed)
# Now move the export-sources to the right location
Expand Down
4 changes: 2 additions & 2 deletions conans/model/conan_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,8 @@ def recipe_metadata_folder(self):
return self.folders.recipe_metadata_folder

@property
def pkg_metadata_folder(self):
return self.folders.pkg_metadata_folder
def package_metadata_folder(self):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a rename, to not use the pkg short version, everything else UI in Conan is package.

return self.folders.package_metadata_folder

@property
def build_path(self) -> Path:
Expand Down
8 changes: 8 additions & 0 deletions conans/model/conanfile_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,18 @@ def options(self):
def recipe_folder(self):
return self._conanfile.recipe_folder

@property
def recipe_metadata_folder(self):
return self._conanfile.recipe_metadata_folder

@property
def package_folder(self):
return self._conanfile.package_folder

@property
def package_metadata_folder(self):
return self._conanfile.package_metadata_folder

@property
def package_path(self) -> Path:
assert self.package_folder is not None, "`package_folder` is `None`"
Expand Down
2 changes: 1 addition & 1 deletion conans/model/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def set_base_recipe_metadata(self, folder):
self._base_recipe_metadata = folder

@property
def pkg_metadata_folder(self):
def package_metadata_folder(self):
return self._base_pkg_metadata

def set_base_pkg_metadata(self, folder):
Expand Down
115 changes: 114 additions & 1 deletion conans/test/integration/metadata/test_metadata_commands.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import os

from conans.test.assets.genconanfile import GenConanfile
from conans.test.utils.tools import TestClient
from conans.test.utils.test_files import temp_folder
from conans.test.utils.tools import TestClient, NO_SETTINGS_PACKAGE_ID
from conans.util.files import save, load


Expand Down Expand Up @@ -85,3 +86,115 @@ def test_update_contents(self):

content = load(os.path.join(metadata_path, "logs", "mylogs.txt"))
assert "mylogs2!!!!" in content

def test_folder_exist(self):
""" so we can cp -R to the metadata folder, having to create the folder in the cache
is weird
"""
c = TestClient(default_server_user=True)
c.save({"conanfile.py": GenConanfile("pkg", "0.1")})
c.run("create .")
c.run("cache path pkg/0.1 --folder=metadata")
metadata_path = str(c.stdout).strip()
assert os.path.isdir(metadata_path)
c.run(f"cache path pkg/0.1:{NO_SETTINGS_PACKAGE_ID} --folder=metadata")
pkg_metadata_path = str(c.stdout).strip()
assert os.path.isdir(pkg_metadata_path)

def test_direct_download_redownload(self):
""" When we directly download things, without "conan install" first, it is also able
to fetch the requested metadata

Also, re-downloading same thing shouldn't fail
"""
c = TestClient(default_server_user=True)
c.save({"conanfile.py": GenConanfile("pkg", "0.1")})
c.run("create .")
pid = c.created_package_id("pkg/0.1")

# Add some metadata
c.run("cache path pkg/0.1 --folder=metadata")
metadata_path = str(c.stdout).strip()
myfile = os.path.join(metadata_path, "logs", "mylogs.txt")
save(myfile, "mylogs!!!!")

c.run(f"cache path pkg/0.1:{pid} --folder=metadata")
pkg_metadata_path = str(c.stdout).strip()
myfile = os.path.join(pkg_metadata_path, "logs", "mybuildlogs.txt")
save(myfile, "mybuildlogs!!!!")

# Now upload everything
c.run("upload * -c -r=default")
assert "pkg/0.1: Recipe metadata: 1 files" in c.out
assert "pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709: Package metadata: 1 files" in c.out

c.run("remove * -c")

# Forcing the download of the metadata of cache-existing things with the "download" command
c.run("download pkg/0.1 -r=default --metadata=*")
assert os.path.isfile(os.path.join(metadata_path, "logs", "mylogs.txt"))
c.run(f"cache path pkg/0.1:{pid} --folder=metadata")
pkg_metadata_path = str(c.stdout).strip()
assert os.path.isfile(os.path.join(pkg_metadata_path, "logs", "mybuildlogs.txt"))

# Re-download shouldn't fail
c.run("download pkg/0.1 -r=default --metadata=*")
assert os.path.isfile(os.path.join(metadata_path, "logs", "mylogs.txt"))
c.run(f"cache path pkg/0.1:{pid} --folder=metadata")
pkg_metadata_path = str(c.stdout).strip()
assert os.path.isfile(os.path.join(pkg_metadata_path, "logs", "mybuildlogs.txt"))

def test_no_download_cached(self):
""" as the metadata can change, no checksum, no revision, cannot be cached
"""
c = TestClient(default_server_user=True)
c.save({"conanfile.py": GenConanfile("pkg", "0.1")})
c.run("create .")
pid = c.created_package_id("pkg/0.1")

# Add some metadata
c.run("cache path pkg/0.1 --folder=metadata")
metadata_path = str(c.stdout).strip()
myrecipefile = os.path.join(metadata_path, "logs", "mylogs.txt")
save(myrecipefile, "mylogs!!!!")

c.run(f"cache path pkg/0.1:{pid} --folder=metadata")
pkg_metadata_path = str(c.stdout).strip()
mypkgfile = os.path.join(pkg_metadata_path, "logs", "mybuildlogs.txt")
save(mypkgfile, "mybuildlogs!!!!")

# Now upload everything
c.run("upload * -c -r=default")
assert "pkg/0.1: Recipe metadata: 1 files" in c.out
assert "pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709: Package metadata: 1 files" in c.out

c2 = TestClient(servers=c.servers)
tmp_folder = temp_folder()
# MOST important part: activate cache
save(c2.cache.new_config_path, f"core.download:download_cache={tmp_folder}\n")

# download package and metadata
c2.run("download pkg/0.1 -r=default --metadata=*")
c2.run("cache path pkg/0.1 --folder=metadata")
c2_metadata_path = str(c2.stdout).strip()
mylogs = load(os.path.join(c2_metadata_path, "logs", "mylogs.txt"))
assert "mylogs!!!!" in mylogs
c2.run(f"cache path pkg/0.1:{pid} --folder=metadata")
c2_pkg_metadata_path = str(c2.stdout).strip()
mybuildlogs = load(os.path.join(c2_pkg_metadata_path, "logs", "mybuildlogs.txt"))
assert "mybuildlogs!!!!" in mybuildlogs

# Now the other client will update the metadata
save(myrecipefile, "mylogs2!!!!")
save(mypkgfile, "mybuildlogs2!!!!")
c.run("upload * -c -r=default --metadata=*")
assert "pkg/0.1: Recipe metadata: 1 files" in c.out
assert "pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709: Package metadata: 1 files" in c.out

# re-download of metadata in c2
c2.run("remove * -c") # to make sure the download cache works
c2.run("download pkg/0.1 -r=default --metadata=*")
mylogs = load(os.path.join(c2_metadata_path, "logs", "mylogs.txt"))
assert "mylogs2!!!!" in mylogs
mybuildlogs = load(os.path.join(c2_pkg_metadata_path, "logs", "mybuildlogs.txt"))
assert "mybuildlogs2!!!!" in mybuildlogs
52 changes: 52 additions & 0 deletions conans/test/integration/metadata/test_metadata_deploy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import textwrap

from conans.test.utils.tools import TestClient


class TestMetadataDeploy:
""" prove we can gather metadata too with a deployer
"""

def test_deploy(self):
# FIXME: It only supports package metadata deployment, because missing internal interface
conanfile = textwrap.dedent("""
import os
from conan import ConanFile
from conan.tools.files import save, copy

class Pkg(ConanFile):
version = "0.1"

def source(self):
save(self, os.path.join(self.recipe_metadata_folder, "logs", "src.log"),
f"srclog {self.name}!!")

def build(self):
save(self, "mylogs.txt", f"some logs {self.name}!!!")
copy(self, "mylogs.txt", src=self.build_folder,
dst=os.path.join(self.package_metadata_folder, "logs"))
""")
deploy = textwrap.dedent("""
import os, shutil

def deploy(graph, output_folder, **kwargs):
conanfile = graph.root.conanfile
for r, d in conanfile.dependencies.items():
shutil.copytree(d.package_metadata_folder, os.path.join(output_folder, "pkgs",
d.ref.name))
# FIXME: Missing
#shutil.copytree(d.recipe_metadata_folder, os.path.join(output_folder, "pkgs",
# d.ref.name))
""")

c = TestClient()
c.save({"conanfile.py": conanfile,
"deploy.py": deploy})
c.run("create . --name=pkg1")
c.run("create . --name=pkg2")
c.run("install --requires=pkg1/0.1 --requires=pkg2/0.1 --deployer=deploy")
assert "some logs pkg1!!!" in c.load("pkgs/pkg1/logs/mylogs.txt")
assert "some logs pkg2!!!" in c.load("pkgs/pkg2/logs/mylogs.txt")
# TODO: This must pass
# assert "srclog pkg1!!!" in c.load("recipes/pkg1/logs/src.log")
# assert "srclog pkg2!!!" in c.load("recipes/pkg2/logs/src.log")
Loading