-
Notifications
You must be signed in to change notification settings - Fork 43
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
Hook: export metadata #21
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
9814213
hook that exports metadata in the 'pre_export' stage
jgsogo dd69e9b
skip test
jgsogo d383281
need to yield, so finally is executed
jgsogo 7e9fbbb
compare item by item
jgsogo 83d5ff4
sort list
jgsogo be3b82d
rename var
jgsogo eef29da
handle remove only
jgsogo 4afd10c
it works with a list of 'exports'
jgsogo 41169df
nest repo fields into its own node
jgsogo File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
# coding=utf-8 | ||
|
||
""" | ||
Export metadata | ||
--------------- | ||
|
||
This hook generates a dictionary with the repository data where the conanfile.py resides, | ||
stores it in a file and exports it with the recipe. It is executed in the `export` command: | ||
|
||
$ conan export . package/0.1.0@user/channel | ||
[HOOK - /Users/jgsogo/dev/conan/conan-hooks/tests/test_hooks/../../hooks/export_metadata.py] pre_export(): Exported metadata to file 'metadata.json' | ||
Exporting package recipe | ||
name/version@jgsogo/test exports: Copied 1 '.json' file: metadata.json | ||
name/version@jgsogo/test: A new conanfile.py version was exported | ||
|
||
This information could be retrieved using the command `get` | ||
|
||
$ conan get package/0.1.0@user/channel metadata.json | ||
|
||
|
||
Note.- The default filename is 'metadata.json', although the user can change it using the | ||
environment variable 'CONAN_HOOK_METADATA_FILENAME' | ||
|
||
""" | ||
|
||
import json | ||
import os | ||
import sys | ||
|
||
import semver | ||
|
||
from conans import __version__ | ||
from conans.errors import ConanException | ||
from conans.tools import Git, save, SVN | ||
|
||
_CONAN_HOOK_METADATA_FILENAME_ENV_VAR = 'CONAN_HOOK_METADATA_FILENAME' | ||
CONAN_HOOK_METADATA_FILENAME = 'metadata.json' | ||
|
||
|
||
def _try_repo_data(path, repo_class): | ||
repo = repo_class(path) | ||
try: | ||
kwargs = {} | ||
if not semver.satisfies(__version__, "<=1.12.x", loose=True): | ||
kwargs.update({'remove_credentials': True}) | ||
danimtb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return {'type': repo_class.cmd_command, | ||
'url': repo.get_remote_url(**kwargs), | ||
'revision': repo.get_revision(), | ||
'dirty': bool(not repo.is_pristine())} | ||
except ConanException: | ||
pass | ||
except Exception as e: | ||
sys.stderr.write("Unhandled error using '{}': {}".format(repo_class, e)) | ||
|
||
|
||
def pre_export(output, conanfile, conanfile_path, *args, **kwargs): | ||
danimtb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" Grab some meta-information from the local folder, write it to a file | ||
and upload it within the recipe | ||
""" | ||
# Check that we are not overriding any file | ||
filename = os.getenv(_CONAN_HOOK_METADATA_FILENAME_ENV_VAR, CONAN_HOOK_METADATA_FILENAME) | ||
target_path = os.path.join(os.path.dirname(conanfile_path), filename) | ||
if os.path.exists(target_path): | ||
output.error("Target file to write metadata already exists: '{}'. Use" | ||
" environment variable '{}' to set a different filename". | ||
format(target_path, _CONAN_HOOK_METADATA_FILENAME_ENV_VAR)) | ||
return | ||
|
||
# Look for the repo | ||
path = os.path.dirname(conanfile_path) | ||
scm_data = _try_repo_data(path, Git) or _try_repo_data(path, SVN) | ||
if not scm_data: | ||
output.warn("Cannot identify a repository system in " | ||
"directory '{}' (tried SVN and Git)".format(path)) | ||
return | ||
|
||
# Build the json | ||
json_data = {'repo': scm_data} | ||
|
||
# Dump information to file and export it with the recipe | ||
content = json.dumps(json_data) | ||
save(target_path, content) | ||
if not getattr(conanfile, 'exports'): | ||
setattr(conanfile, 'exports', os.path.basename(filename)) | ||
else: | ||
conanfile.exports = conanfile.exports, os.path.basename(filename) | ||
jgsogo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
output.info("Exported metadata to file '{}'".format(filename)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
# coding=utf-8 | ||
|
||
import json | ||
import os | ||
import subprocess | ||
import sys | ||
import textwrap | ||
import unittest | ||
|
||
import six | ||
from parameterized import parameterized | ||
from six.moves.urllib.parse import quote | ||
|
||
from conans import tools | ||
from conans.paths import CONAN_MANIFEST | ||
from tests.utils import capabilities | ||
from tests.utils.test_cases.conan_client import ConanClientTestCase | ||
|
||
here = os.path.dirname(__file__) | ||
|
||
|
||
def get_export_metadata_vars(): | ||
sys.path.insert(0, os.path.join(here, '..', '..', 'hooks')) | ||
try: | ||
from export_metadata import CONAN_HOOK_METADATA_FILENAME, \ | ||
_CONAN_HOOK_METADATA_FILENAME_ENV_VAR | ||
return CONAN_HOOK_METADATA_FILENAME, _CONAN_HOOK_METADATA_FILENAME_ENV_VAR | ||
finally: | ||
sys.path.pop(0) | ||
|
||
|
||
METADATA_FILENAME, METADATA_FILENAME_ENV_VAR = get_export_metadata_vars() | ||
|
||
|
||
class ExportMetadataTests(ConanClientTestCase): | ||
conanfile_base = textwrap.dedent("""\ | ||
from conans import ConanFile | ||
|
||
class AConan(ConanFile): | ||
{exports} | ||
pass | ||
""") | ||
conanfile_plain = conanfile_base.format(exports="") | ||
|
||
def _get_environ(self, **kwargs): | ||
kwargs = super(ExportMetadataTests, self)._get_environ(**kwargs) | ||
kwargs.update({'CONAN_HOOKS': os.path.join(here, '..', '..', 'hooks', 'export_metadata')}) | ||
return kwargs | ||
|
||
def test_no_repo(self): | ||
tools.save('conanfile.py', content=self.conanfile_plain) | ||
output = self.conan(['export', '.', 'name/version@jgsogo/test']) | ||
self.assertIn("pre_export(): WARN: Cannot identify a repository system in directory", output) | ||
|
||
def test_conflicting_file(self): | ||
tools.save('conanfile.py', content=self.conanfile_plain) | ||
tools.save(METADATA_FILENAME, content='whatever') | ||
output = self.conan(['export', '.', 'name/version@jgsogo/test']) | ||
self.assertIn("ERROR: Target file to write metadata already exists", output) | ||
self.assertIn("Use environment variable '{}' to set a different " | ||
"filename".format(METADATA_FILENAME_ENV_VAR), output) | ||
|
||
with tools.environment_append({METADATA_FILENAME_ENV_VAR: "other.json"}): | ||
output = self.conan(['export', '.', 'name/version@jgsogo/test']) | ||
self.assertNotIn("ERROR: Target file to write metadata already exists", output) | ||
|
||
@parameterized.expand([('"myfile.txt"',), ('("myfile1", "myfile2")',), (None, )]) | ||
def test_git_repository(self, exports): | ||
conanfile = self.conanfile_base.format(exports=exports) if exports else self.conanfile_plain | ||
reference = 'name/version@jgsogo/test' | ||
url = 'http://some.url' | ||
|
||
git = tools.Git() | ||
git.run("init .") | ||
git.run('config user.email "you@example.com"') | ||
git.run('config user.name "Your Name"') | ||
tools.save('conanfile.py', content=conanfile) | ||
git.run('add --all') | ||
git.run('commit -am "initial"') | ||
git.run('remote add origin {}'.format(url)) | ||
|
||
output = self.conan(['export', '.', reference]) | ||
self.assertIn("pre_export(): Exported metadata to file '{}'".format(METADATA_FILENAME), | ||
output) | ||
|
||
# Check that the file is in the export folder, contains expected data and is in the manifest | ||
output = self.conan(['get', reference, METADATA_FILENAME]) | ||
data = json.loads(output) | ||
self.assertDictEqual(data['repo'], {'type': 'git', | ||
'url': url, | ||
'revision': git.get_commit(), | ||
'dirty': False}) | ||
output = self.conan(['get', reference, CONAN_MANIFEST]) | ||
self.assertIn(METADATA_FILENAME, output) | ||
|
||
@parameterized.expand([(True,), (False,)]) | ||
@unittest.skipUnless(capabilities.svn(), "SVN not available") | ||
def test_svn_repository(self, pristine_repo): | ||
if pristine_repo and not bool(tools.SVN.get_version() >= tools.SVN.API_CHANGE_VERSION): | ||
raise unittest.SkipTest("Required SVN >= {} to test for pristine " | ||
"repo".format(tools.SVN.API_CHANGE_VERSION)) | ||
|
||
reference = 'name/version@jgsogo/test' | ||
|
||
# Create the SVN repo | ||
repo_url = self._gimme_tmp() | ||
subprocess.check_output('svnadmin create "{}"'.format(repo_url), shell=True) | ||
repo_url = tools.SVN.file_protocol + quote(repo_url.replace("\\", "/"), safe='/:') | ||
|
||
# Create the working repo | ||
svn = tools.SVN() | ||
svn.checkout(url=repo_url) | ||
tools.save('conanfile.py', content=self.conanfile_plain) | ||
svn.run("add conanfile.py") | ||
svn.run('commit -m "initial"') | ||
if pristine_repo: | ||
svn.run('update') | ||
self.assertEqual(svn.is_pristine(), pristine_repo) | ||
|
||
output = self.conan(['export', '.', reference]) | ||
self.assertIn("pre_export(): Exported metadata to file '{}'".format(METADATA_FILENAME), | ||
output) | ||
|
||
# Check that the file is in the export folder, contains expected data and is in the manifest | ||
output = self.conan(['get', reference, METADATA_FILENAME]) | ||
data = json.loads(output) | ||
self.assertListEqual(sorted(data['repo'].keys()), | ||
sorted([six.u('type'), six.u('url'), | ||
six.u('revision'), six.u('dirty')])) | ||
self.assertEqual(data['repo']['type'], six.u('svn')) | ||
self.assertEqual(data['repo']['url'].lower(), six.u(repo_url).lower()) | ||
self.assertEqual(data['repo']['revision'], svn.get_revision()) | ||
self.assertEqual(data['repo']['dirty'], bool(not pristine_repo)) | ||
|
||
output = self.conan(['get', reference, CONAN_MANIFEST]) | ||
self.assertIn(METADATA_FILENAME, output) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# coding=utf-8 | ||
|
||
from contextlib import contextmanager | ||
|
||
from conans import __version__ as conan_version | ||
from conans.client.command import Conan, CommandOutputer, Command | ||
from conans.model.version import Version | ||
|
||
|
||
@contextmanager | ||
def conan_command(output_stream): | ||
# This snippet reproduces code from conans.client.command.main, we cannot directly | ||
# use it because in case of error it is exiting the python interpreter :/ | ||
conan_api, cache, user_io = Conan.factory() | ||
user_io.out._stream = output_stream | ||
outputer = CommandOutputer(user_io, cache) | ||
cmd = Command(conan_api, cache, user_io, outputer) | ||
try: | ||
yield cmd | ||
finally: | ||
if Version(conan_version) < "1.13": | ||
conan_api._remote_manager._auth_manager._localdb.connection.close() # Close sqlite3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not direct comparison using Version class? Is
loose
necessary?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Try it a little bit, it was not easy to get a working expression, but I would be very pleased if you suggest something simpler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure, but something like this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Version("1.12.0-dev") >= "1.12.0"
❌There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about... ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could work, but
conans.model.version.Version
doesn't belong to the public Conan API 😝There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😆 😆 😆 😆
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still don't know why you are not using the Version class as it is something we are somehow recommending fo recipes...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is an open issue about
conans.model.Version
that is proposing to define an API for that class. The operator>=
could dissapear in favor of asatisfies
function like in SemVer, and then this hook (production code) will be broken requiring an update from the user... I prefer to rely on public APIs, once it is defined we can change it.