repo: setup a foreign arch and sources #1348

Closed
wants to merge 28 commits into
from

Conversation

Projects
None yet
4 participants
Collaborator

kalikiana commented Jun 2, 2017

Automatically add the target architecture via dpkg and setup apt sources.list for multi-arch if needed.

  • Print host setup instructions for foreign architectures and sources list.
  • Distinguish invalid architectures.

Fixes: bug 1660903

Follow-up on Figure out multi-arch build dependencies

@kalikiana kalikiana changed the title from Foreign arch src to Setup a foreign arch and sources Jun 2, 2017

@kalikiana kalikiana self-assigned this Jun 2, 2017

snapcraft/internal/repo/_deb.py
+ def _setup_foreign_arch(cls, arch):
+ native_arch = subprocess.check_output(
+ ['dpkg', '--print-architecture']).decode(
+ sys.getfilesystemencoding())
@elopio

elopio Jun 14, 2017

Member

hum, we are hardcoding 'utf-8' everywhere. This seems better.

@kyrofa

kyrofa Jun 14, 2017

Member

Agreed.

snapcraft/internal/repo/_deb.py
+ sources_arch_file.flush()
+ sources_lists = os.path.join('/etc/apt/sources.list.d/',
+ 'ubuntu-{}.list'.format(arch))
+ subprocess.check_call(['sudo', 'cp',
@elopio

elopio Jun 14, 2017

Member

Why not mv, so the temp file is not left hanging there?

@kalikiana

kalikiana Jun 14, 2017

Collaborator

The file is deleted at the end of the with statement.

I reviewed the last commit, and I like it. Do you think this is something Colin should review?

And maybe a manual test?

@kalikiana kalikiana changed the title from Setup a foreign arch and sources to repo: setup a foreign arch and sources Aug 8, 2017

@sergiusens sergiusens added this to the 2.35 milestone Aug 10, 2017

snapcraft/internal/repo/_deb.py
+ """If the given package has an arch suffix:
+ Generate arch-specific sources list
+ Update the package cache to pull in the new packages
+ Verify that the package is in the cache
@kyrofa

kyrofa Sep 8, 2017

Member

It's not completely clear from the function name or the docstring what the return value indicates.

A few suggestions.

snapcraft/internal/repo/__init__.py
@@ -34,11 +34,19 @@ def check_for_command(command):
def get_pkg_name_parts(pkg_name):
@kyrofa

kyrofa Sep 8, 2017

Member

This is getting complex, and looks error-prone. I can imagine myself asking "what's the second arg... version or arch?" and "the arch doesn't include the colon, right?"

I also see us using name + arch a lot below. I suggest making this a super simple class with a few helper methods to do that type of thing for us, and return an instance of that class here.

@kalikiana

kalikiana Sep 15, 2017

Collaborator

I dropped those changes from this PR, after moving to using the error message to render instructions needed to setup the host.

snapcraft/internal/repo/_deb.py
+ return
+ logger.info('Adding foreign architecture {}'.format(arch))
+ subprocess.check_output(
+ ['sudo', 'dpkg', '--add-architecture', arch])
@kyrofa

kyrofa Sep 8, 2017

Member

I know I've said this before, but I'll add my voice here again: I personally don't like this. It means running the wrong snapcraft.yaml on my host will garbage up my arch list and sources. The only place this makes sense is when we're building in containers by default, but we're not there yet. Is there any way we can just warn if we're not building in a container?

@kalikiana

kalikiana Sep 12, 2017

Collaborator

Warn and then what? The build will fail.

@kyrofa

kyrofa Sep 12, 2017

Member

Yes, that's exactly what I want. Warn telling me what I need to do if I actually want to build this snap (e.g. this command). Ahem I suppose that would be an error, not a warning.

@kalikiana

kalikiana Sep 14, 2017

Collaborator

Ah, yeah. I see what you're saying now. We could basically generate the necessary commands and leave it up to you to decide which way to go - to my mind, that's the crucial bit, make it as discoverable as possible what's needed to continue, since it's just tedious to find this out for yourself if you've never done it.

How about something like The host needs to be setup for the target architecture. You can use cleanbuild or set SNAPCRAFT_CONTAINER_BUILDS=1 to build in a container that's automatically set up for cross-compiling. To setup the local machine, run the following commands: [...].
Long term with #1458 we could also consider a prompt like Building in a container is recommended for cross-compiling [...] [y/n]

@kyrofa

kyrofa Sep 14, 2017

Member

Yeah that's a great error, +1.

snapcraft/internal/repo/_deb.py
with apt.Cache() as apt_cache:
try:
cls._mark_install(apt_cache, package_names)
except errors.PackageNotFoundError as e:
- raise errors.BuildPackageNotFoundError(e)
+ if cls._setup_multi_arch_sources(apt_cache, e.package):
+ pass # FIXME?
@kyrofa

kyrofa Sep 8, 2017

Member

The PR description doesn't indicate that further work needs to be done. The FIXME isn't particularly helpful, either. What happens if a release contains this before it's fixed? Should we raise an NotImplementedError or something instead?

@kalikiana

kalikiana Sep 12, 2017

Collaborator

Good catch, I didn't mean to leave that there. It was supposed to be a reminder for myself as I had to re-think the code after merging, but I apparently forgot. Will fix it.

snapcraft/internal/repo/_deb.py
@@ -413,7 +533,7 @@ def _format_sources_list(sources_list, *,
'release': release,
'suffix': suffix,
'security': security,
- })
+ }).replace('deb', 'deb [arch={}]'.format(deb_arch) if foreign else 'deb')
@kyrofa

kyrofa Sep 8, 2017

Member

I'm curious why we're replacing here as opposed to making it a templated param? We should probably be catching KeyErrors here, by the way, and turning them into warnings since this can come from custom plugins as well.

Assuming we went this route for a good reason, we need to make sure it's robust to sources like deb http://debian.mycompany.com/[...].

@kalikiana

kalikiana Sep 14, 2017

Collaborator

I sorta considered this internal API used only but Snapcraft itself. But actually you're right, this would benefit from some error handling. The replace was meant to avoid having [arch= in there when it's not needed, but I should use an $arch variable for that.

@kyrofa

kyrofa Sep 14, 2017

Member

Or just use a $deb variable for the whole thing, yeah, something like that.

snapcraft/internal/repo/_deb.py
+ })
+ except KeyError as e:
+ raise errors.SnapcraftError(
+ 'Sources list is missing the variable ${' + str(e) + '}')
@kyrofa

kyrofa Sep 15, 2017

Member

I fell prey to this as well. This error message is a little backward: a KeyError is raised if the template is not satisfied, not if a substitution was requested but not performed. Code will be more clear:

>>> import string
>>> t = '$food is delicious'
>>> string.Template(t).substitute(food='sushi')
'sushi is delicious'
>>> string.Template(t).substitute(food='sushi', bar='baz')
'sushi is delicious'
>>> string.Template(t).substitute(bar='baz')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.5/string.py", line 129, in substitute
    return self.pattern.sub(convert, self.template)
  File "/usr/lib/python3.5/string.py", line 119, in convert
    val = mapping[named]
KeyError: 'food'

So the error should probably be something more along the lines of "Cannot complete substitution in sources list: unable to define $foo".

@kyrofa

kyrofa Sep 15, 2017

Member

There doesn't seem to be a way to warn in the reverse situation... hmm...

@kalikiana

kalikiana Sep 19, 2017

Collaborator

You made me wonder what's actually an error here: if you're using a varaible ${foo} that we don't know how to fill in, that seems wrong - but using all the variables isn't necessary.

So, I updated the error based on your wording suggestion and added two test cases for the above mentioned cases, one of them failing, the other one passing. It seems to make sense this way... but let me know what you think.

snapcraft/internal/repo/_deb.py
+ @property
+ def message(self):
+ message = super().message
+ # If the package was multiarch, try to help.
@kyrofa

kyrofa Sep 15, 2017

Member

This looks like it's building on what used to be in errors.PackageNotFoundError. What is the rational for essentially redefining it here as opposed to moving this logic back into it?

@kalikiana

kalikiana Sep 18, 2017

Collaborator

What do you mean by redefining?

My idea was to move the logic into the error because it needs to know

  • if the arch is valid
  • if the arch was registered
  • if there's sources for the arch
    _mark_install doesn't need to make a distinction if we always want it to fail - previously I had the code in there because it would have had to continue iterating through the packages after automatically setting up the host system.

Also, we might be able to re-use the error with all its details. There's a case of except PackageNotFoundError as e: raise BuildPackageNotFoundError and another instance of raising PackageNotFoundError. Maybe this could be a follow-up.

Maybe having helper methods like has_arch still makes sense when I'm adding the container side of things, but aside from that I feel it would become less readable to do that in the same method as figuring out an error message.

@kalikiana

kalikiana Sep 18, 2017

Collaborator

Ha, after typing all this, and reading your question once more, I realize you're probably referring to the fact that _deb.PackageNotFoundError is a subclass.
I did that because the logic is completely specific to Debian-based distros and the is this a deb distro-check is implied by being inside _deb as well. Presumably once there's a Fedora/dnf backend it will have completely different code (or none at all) depending on how architectures are handled there.

@kyrofa

kyrofa Sep 18, 2017

Member

Ah, good call, that makes sense indeed.

snapcraft/internal/repo/_deb.py
- 'Sources list is missing the variable ${' + str(e) + '}')
+ raise ValueError(
+ 'Cannot complete substitution in sources list: '
+ 'unknown variable ${' + str(e.args[0]) + '} in template')
@kyrofa

kyrofa Sep 19, 2017

Member

Looks good, one nit: the format for a variable doesn't REQUIRE braces, it simply supports them. I wonder if simply using the variable name here would be better instead of assuming a specific format.

@kalikiana

kalikiana Sep 21, 2017

Collaborator

I was kinda going by the fact that we use braces in our templates... but I thought of another option, I'm checking now which one was actually used to refer to it in the error message.

Collaborator

sergiusens commented Oct 9, 2017

stale

@sergiusens sergiusens closed this Oct 9, 2017

Collaborator

kalikiana commented Oct 10, 2017

Why did you close this? It's not stale: the last commit, addressing the test failure, is from Sunday.

@kalikiana kalikiana reopened this Oct 11, 2017

Collaborator

kalikiana commented Oct 11, 2017

TypeError: <lambda>() got an unexpected keyword argument 'stdout'

@kalikiana kalikiana closed this Oct 11, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment