-
Notifications
You must be signed in to change notification settings - Fork 3
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
Add noarch support to the installer #9
Conversation
Outstanding issues:
|
conda_rpms/install.py
Outdated
match = re.search('python-(\d+.\d+).\d+-\d+', dist) | ||
if match: | ||
py_ver = match.group(1) | ||
return py_ver |
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.
I used this method as that is how conda
I couldn't copy a function from conda so I just wrote a simplified version of what is being done (without all the extra conda-ness)
An explanation since regexp is horrible:
This searches for a package called something like python-2.7.6-3
and from that returns the group 2.7
the \d
means digits (0-9), the +
means there can be any number of digits and so it can also match for things like python-412.23.1-5645654654
for which it would return 412.23
as the python version
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.
Looking good. I see what you mean about struggling to integrate it tidily, but this was always going to be quite an invasive change I think.
for dist in linked(prefix): | ||
match = re.search('python-(\d+.\d+).\d+-\d+', dist) | ||
if match: | ||
py_ver = match.group(1) |
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.
Personally, I'd put an else on this for loop to catch an issue with python not being installed.
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.
Alright
I have encountered the question of what to do with errors frequently in this.
I don't want an exception to be raised as then that would break the RPM installation (I think). I would prefer it to just fail silently, but then for us to be able to see why it failed by looking at logs (not that we can see them).
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.
I'd put an else on this for loop to catch an issue with python not being installed
How would you handle Python not being installed.
Is it enough to add something to the log that say "Python not already linked in the env"?
conda_rpms/install.py
Outdated
noarch = False | ||
# If the distribution is noarch, it will contain a `link.json` file in | ||
# the info_dir | ||
link_json = join(info_dir, 'link.json') |
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.
This seems shaky. Can't we inspect the metadata in info.json (or whatever it is called)? The bit that says noarch: python
?
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.
We can check that noarch: python
exists in the index.json
of the metadata but we do need to get the information about the entrypoints from the link.json
conda_rpms/install.py
Outdated
target_site_packages) | ||
dst = join(prefix, noarch_f) | ||
else: | ||
dst = join(prefix, f) |
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.
Is this really all we need to do for noarch: whatever
packages? Can we be a bit more defensive on 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.
Not sure what you mean by this.
There is some more noarch specific installtion steps later on (entrypoints and py compilation) but they have to be done after this
fecd087
to
faa4e91
Compare
@@ -516,18 +697,55 @@ def link(pkgs_dir, prefix, dist, linktype=LINK_HARD, index=None, target_prefix=N | |||
return | |||
|
|||
source_dir = join(pkgs_dir, dist) | |||
if not run_script(dist, 'pre-link', prefix, target_prefix): | |||
if not run_script(prefix, dist, 'pre-link', target_prefix): |
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.
This was just a bug
1ab463f
to
feb1e81
Compare
ab2f125
to
b595ff3
Compare
(cherry picked from commit c40f1b49bb2624d416f88daea37e0cce0f15731f)
b595ff3
to
49216f2
Compare
a188d39
to
c4e993d
Compare
1ec0504
to
bdff5cf
Compare
conda_rpms/build_rpm_structure.py
Outdated
@@ -143,11 +139,29 @@ def create_rpmbuild_for_tag(repo, tag_name, target, config): | |||
with open(spec_fname, 'r') as fh: | |||
env_spec = yaml.safe_load(fh).get('env', []) | |||
create_rpmbuild_for_env(manifest, target, config) | |||
pkgs = [pkg for _, pkg in manifest] | |||
|
|||
index = conda.fetch.fetch_index(list(set([url for url, pkg in manifest])), |
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.
The unpacked pkg
is not used, so you could replace it with _
...
anaconda_url = 'https://conda.anaconda.org/' | ||
if url.startswith(anaconda_url): | ||
url = url[len(anaconda_url):] | ||
dists.append('::'.join([os.path.dirname(url), pkg])) |
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.
So, if I understand this right ... your influencing the sort here by chopping out the https://conda.anaconda.org/
from the URL to equalize with items in the manifest that have a channel name rather than a full URL? i.e. https://conda.anaconda/org/wibble/wobble/pkg
becomes wibble/wobble::pkg
? and that makes it a fairer sort against conda-forge::pkg
...
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.
This doesn't influence the sort
The function that does the ordering requires an input to be of the format channel::pkg
. If a channel is not on anaconda, it uses the full url. If the channel is on anaconda, it uses just the channel name.
It is like when you do a conda list. Packages from the defaults channel or conda-forge say defaults
or conda-forge
however other channels (e.g. our internal channels) have the full url.
conda_rpms/install.py
Outdated
import shlex | ||
from os.path import abspath, basename, dirname, isdir, isfile, islink, join | ||
from os.path import abspath, basename, dirname, exists, isdir, isfile, islink, \ | ||
join, split, splitext |
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.
@lbdreyer Do we care about sorting the import order here?
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. It is a copy of the conda/install.py and I wanted to make minimal changes, but I also wanted to have minimal code necessary (rather than it be some long thing).
But I think we could reorder the imports, as long as we put in a comment saying that we did that.
conda_rpms/install.py
Outdated
* changed log.trace -> log.info (log.trace not supported) | ||
|
||
""" | ||
command = [python_exe_full_path, '-Wi', '-m', 'py_compile', py_full_path] |
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.
@lbdreyer This isn't exactly the same ... kinda is, but missing the quotation around python_exe_full_path
and py_full_path
when substituted into the command
i.e.
command = '"%s" -Wi -m py_compile "%s"' % (python_exe_full_path, py_full_path)
So within conda
, if we had python_exe_full_path = "wibble"
and py_full_path = "wobble"
we'd get a command of "wibble" -Wi -m "wobble"
BUT the modified change we get wibble -Wi -m wobble
... don't know if this really matters or not, perhaps not.
conda_rpms/install.py
Outdated
subprocess.call(command) | ||
|
||
if not isfile(pyc_full_path): | ||
log.info('{} failed to compile correctly'.format(pyc_full_path)) |
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.
@lbdreyer The original log message was kinda more helpful i.e.
message = """
pyc file failed to compile successfully
python_exe_full_path: %()s\n
py_full_path: %()s\n
pyc_full_path: %()s\n
"""
log.info(message, python_exe_full_path, py_full_path, pyc_full_path)
Did we want to add more detail here incase it barfs?
conda_rpms/install.py
Outdated
|
||
def parse_entry_point_def(ep_definition): | ||
""" | ||
Copy of conda/core.path.py:parse_entry_point_def at |
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.
This is conda/common/path.py:parse_entry_point_def
conda_rpms/install.py
Outdated
br'(?:[ ]*)' # allow spaces between #! and beginning of the executable path | ||
br'(/(?:\\ |[^ \n\r\t])*)' # the executable is the next text block without an escaped space or non-space whitespace character # NOQA | ||
br'(.*)' # the rest of the line can contain option flags | ||
br')$') # end whole_shebang group |
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.
@lbdreyer make this global to this module i.e. remove the definition of SHEBANG_REGEX
here. Is there a reason why you've made it local?
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.
Only made it local because thought it didn't need to be global and I thought it would be better if it was closer to where it was being used to make it clear it was part of the noarch changes but I think there is a good place I can move it to.
at be8c08c083f4d5e05b06bd2689d2cd0d410c2ffe. | ||
|
||
Modifications included: | ||
* removed mode check for non-binary shebang |
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.
@lbdreyer Why can't we support mode
?
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 seemed a bit pointless. mode
is included in the original function because the function would have been reused in lots of places.
In this install.py
script, this function is only called by create_python_entry_point
and if you look at the original function, in the call to create_python_entry_point
it sets the mode to be FileMode.text
so there just doesn't seem to be much point
conda_rpms/install.py
Outdated
at be8c08c083f4d5e05b06bd2689d2cd0d410c2ffe. | ||
|
||
Modifications included: | ||
* Removed check that the entry point already exists |
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.
@lbdreyer Why did you remove the check?
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.
I can't remember. I will put it back.
I was trying to keep the code as minimal but it is actually quite a small addition
conda_rpms/install.py
Outdated
|
||
if hasattr(shebang, 'decode'): | ||
shebang = shebang.decode() | ||
|
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.
@lbdreyer Nuke the blank line
return target_full_path | ||
|
||
|
||
def get_python_version(prefix): |
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.
@lbdreyer Is this your own function or is it sourced from conda
?
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.
Mine, if it was sourced from conda I stated that in the function docstring
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.
Cool, thanks!
with open(join(info_dir, 'index.json'), 'r') as fh: | ||
index_data = json.loads(fh.read()) | ||
if 'noarch' in index_data: | ||
noarch = index_data['noarch'] |
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 is possible that noarch
could be set to python
or generic
. I don't think that we'd care about generic
, but you never know.
Also, do we support the legacy noarch_python: True
recipe YAML ... if so, then we should have:
if 'noarch' in index_data:
noarch = index_data['noarch']
elif 'noarch_python' in index_data:
noarch = index_data['noarch_python']
if noarch:
if on_win:
raise ValueError(...)
....
@@ -546,6 +777,30 @@ def link(pkgs_dir, prefix, dist, linktype=LINK_HARD, index=None, target_prefix=N | |||
log.error('failed to link (src=%r, dst=%r, type=%r, error=%r)' % | |||
(src, dst, lt, e)) | |||
|
|||
# noarch package specific installation steps | |||
if noarch == 'python': |
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.
conda_rpms/install.py
Outdated
dst = join(prefix, noarch_f) | ||
all_files.append(noarch_f) | ||
else: | ||
dst = join(prefix, f) |
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.
@lbdreyer Add a comment to make it crystal clear that this if
branch is for non-noarch.
@lbdreyer I'm kinda done now. I'll spin up early tomorrow morning and go over the testing, but I think this is looking really, really good! Well done! I've made a bunch of comments, but they're all really pretty minor. Also, it's interesting that |
I have put a commit with changes based on the reviews actions so far |
@@ -7,7 +7,7 @@ export INSTALL_ROOT=/opt/conda | |||
REPO_ROOT=$(cd "$(dirname ${0})/../../.."; pwd;) | |||
|
|||
# Test conda-gitenv approach. | |||
cat << EOF | docker run -i \ | |||
cat << 'EOF' | docker run -i \ |
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.
@lbdreyer Careful.
In this case we do want variable substitution to happen in the here document, as it makes reference to environment variables OUTDIR
, ENV_REPO
and INSTALL_ROOT
.
Also, note that the PATH
environment variable is escaped, on line 19, so that will be substituted within the container at runtime.
So, don't quote EOF
wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh --no-verbose | ||
bash Miniconda3-latest-Linux-x86_64.sh -b -p ${MINICONDA_DIR} && rm -f Miniconda*.sh | ||
|
||
export PATH=$MINICONDA_DIR/bin:$PATH |
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.
export PATH=${MINICONDA_DIR}/bin:${PATH}
""" | ||
|
||
if os.path.lexists(target_full_path): | ||
warnings.warn('Entrypoint {} already exists.'.format(target_full_path)) |
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.
I did this so that it would keep going even if the entrypoint already exists.
That way the rpm installation doesn't just fail, but if something does go wrong we can look at the logs and see that this had happened
# Get the name of the noarch_package, e.g. tqdm-4.19.8-py_0 | ||
noarch_tqdm=$(basename $(find $MINICONDA_DIR/pkgs -type d -name "tqdm-*-py*")) | ||
echo $noarch_tqdm | ||
python /repo/conda_rpms/install.py --pkgs-dir=$MINICONDA_DIR/pkgs --prefix=$MINICONDA_DIR/envs/test-env --link $noarch_tqdm |
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.
@lbdreyer Fancy enclosing all environment variables in {...}
?
# ---------------------------------------------------- | ||
# This will ensure that the noarch package and all it's dependencies are | ||
# downloaded and in the cache. | ||
conda create -n test-env tqdm -c conda-forge |
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.
Agreed to pin the tqdm
version to what we need and want
conda create -n test-env tqdm -c conda-forge | ||
source activate test-env | ||
conda uninstall tqdm | ||
source deactivate |
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.
@lbdreyer I'm assuming that you're installing and then uninstalling just to populate the package cache with the tqdm
tarball?
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.
You could do a quick check here to ensure that tqdm
is not in the site-packages
directory and its associated entry point has been uninstalled - this ensures that the following install.py
test is valid.
python -c "import tqdm" | ||
|
||
echo 'Check the entrypoint exists' | ||
which tqdm |
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.
@lbdreyer If the which
or python -c "import tqdm"
fail, does this make the whole test fail?
Just want to make sure that as far as circle-ci
is concerned, the test doesn't always pass, just because it ran.
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.
So i tested this.
You can see that a bad which
fails the whole job in the output here:
https://circleci.com/gh/lbdreyer/conda-rpms/66
and you can see that a bad python -c "import ..."
fails here:
https://circleci.com/gh/lbdreyer/conda-rpms/67
@lbdreyer Just checked what happens for uninstalling, and basically for a It's not that we're supporting uninstalling individual packages, but uninstalling complete environments, which is easier. So, that said, I think we're all good for uninstalling - nothing extra to do. |
# Create fake env.spec and manifest files | ||
with open(temp_repo + '/env.spec', 'w') as env_spec_file: | ||
env_spec_file.write(ENV_SPEC) | ||
with open(temp_repo + '/env.manifest', |
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.
@lbdreyer Use os.path.join
here and above in line 34
# Parse the lines of the spec file that are formatted as | ||
# ` {INSTALL} xz-5.2.3-0\n` | ||
if line.startswith(' ${INSTALL}'): | ||
result_order.append(line.split(' ')[3][:-1]) |
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.
@lbdreyer We're kinda being held to ransom by the format of the line and readlines
tacking on a \n
, which make the indexing kinda non-intuitive. How about:
for line in fh.readlines():
line = line.strip()
if line.startswith('${INSTALL}'):
result_order.append(line.split(' ')[1])
@lbdreyer Okay, over to you ... |
9f9cec7
to
a975653
Compare
6377daf
to
3a90a41
Compare
Okay @bjlittle I have now dealt with all the review actions. I am going to run the testing in the docker container |
circle.yml
Outdated
- ./conda_rpms/tests/integration/gitenv_create_rpmbuild.sh | ||
- ./conda_rpms/tests/integration/gitenv_build_rpms.sh | ||
# - ./conda_rpms/tests/integration/gitenv_create_rpmbuild.sh | ||
#- ./conda_rpms/tests/integration/gitenv_build_rpms.sh |
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.
Yup, makes sense these integration tests are failing anyways 👍
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.
Argh sorry. I only actually did this cause I didn't want to have to wait for these tests to run. I'll un-comment these so it's back to how it was
conda_rpms/install.py
Outdated
if index_data['noarch_python'] is True: | ||
noarch = 'python' | ||
|
||
if noarch: |
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.
@lbdreyer I guess this should be if noarch == 'python'
then...
with open(os.path.join(temp_repo + '/env.spec'), 'w') as \ | ||
env_spec_file: | ||
env_spec_file.write(ENV_SPEC) | ||
with open(os.path.join(temp_repo + '/env.manifest'), 'w') as \ |
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.
@lbdreyer Note that you don't need the leading /
as os.path.join
will do this for you.
@lbdreyer After you're satisfied all is well after testing (apologies, added another couple of minor comments) I'll squash and merge! 😄 How exciting! |
@bjlittle Okay. I've tested it now and left the container running if you'd like to have a look. |
with open(os.path.join(temp_repo + 'env.spec'), 'w') as \ | ||
env_spec_file: | ||
env_spec_file.write(ENV_SPEC) | ||
with open(os.path.join(temp_repo + 'env.manifest'), 'w') as \ |
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.
@lbdreyer I almost merged, but thought I give the code just one last skim through ...
This should be os.path.join(temp_repo, 'env.spec')
and os.path.join(temp_repo, 'env.manifest')
... we need to use ,
not +
.
But surely, this would have caused the testing to fail?
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.
@lbdreyer Perhaps I'm being dense (again) but I can't see how the unit
tests are being run as any part of the CI, can you?
I assume that you can run the unit
tests within your development environment?
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.
If you need to run the unit
tests by hand, then just confirm that they work then create an issue to ensure that we add unit
testing to the CI (via travis-ci I guess) at a later date.
I'm keen for us to get this merged 👍
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.
Good call, I hadn't done the os.path.join correctly
I fixed it in the last commit and you can see the unit test passed:
$ python -m unittest -v conda_rpms.tests.unit.build_rpm_structure.test_create_rpmbuild_for_tag
test_sorted_order
...
ok
----------------------------------------------------------------------
Ran 1 test in 5.960s
OK
I ran all the tests and I got one failure:
.....E...........
======================================================================
ERROR: test_pkg_all_linked (conda_rpms.tests.unit.build_rpm_structure.test_create_rpmbuild_for_env.Test)
----------------------------------------------------------------------
Traceback (most recent call last):
File "../conda-rpms/conda_rpms/tests/unit/build_rpm_structure/test_create_rpmbuild_for_env.py", line 19, in test_pkg_all_linked
with patch(func, return_value=zip(*self.pkgs)[1]):
TypeError: 'zip' object is not subscriptable
----------------------------------------------------------------------
Ran 17 tests in 13.076s
FAILED (errors=1)
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.
I have raised the issue for unit tests being added to CI:
#11
@lbdreyer Well done! 🍺 🎉 🎈 👍 |
Great job on this @lbdreyer. Pretty intense stuff, and it looks really comprehensive 👍 🎉 |
Conda introduced noarch python packages. You can tell that the binary is noarch as it will have a 'link.json' with information about the noarch in the info directory of the binary (once it's been extracted).
To install a noarch python package, there are extra steps that need to be taken:
This PR modifies the 'link' function which gets run when we install RPMs. now it checks that the package is noarch and if so does the above 4 extra steps. These extra steps are done using copies of the relevant functions taken from the most recent of conda. I have slightly had to modify these.