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

feat: support workspaces for go #864

Merged
merged 22 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
8 changes: 5 additions & 3 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ jobs:
echo "::group::apt install"
sudo apt install -y ninja-build cmake scons \
autoconf automake autopoint gcc git gperf help2man libtool texinfo
# Remove newer go and install regular version for 20.04
sudo apt install -y golang
# Install go from the Snap Store
go_job=$(sudo snap install --no-wait --classic go)
# Install RPM dependencies for RPM tests
sudo apt install rpm
# Install poetry. From pipx on focal, from apt on newer systems.
Expand All @@ -96,6 +96,7 @@ jobs:
else
sudo apt-get install -y python3-poetry
fi
snap watch $go_job
# Ensure we don't have dotnet installed, to properly test dotnet-deps
# Based on https://github.com/actions/runner-images/blob/main/images/linux/scripts/installers/dotnetcore-sdk.sh
sudo apt remove -y dotnet-* || true
Expand Down Expand Up @@ -159,11 +160,12 @@ jobs:
echo "::group::snap install"
sudo snap install --no-wait core20
sudo snap install chisel --channel latest/candidate --no-wait
sudo snap install go --classic --no-wait
echo "::endgroup::"
echo "::group::apt install"
sudo apt install -y ninja-build cmake scons qt5-qmake p7zip \
autoconf automake autopoint gcc git gperf help2man libtool texinfo \
curl findutils pkg-config golang rpm \
curl findutils pkg-config rpm \
findutils python3-dev python3-venv
echo "::endgroup::"
echo "::group::dotnet removal"
Expand Down
13 changes: 12 additions & 1 deletion craft_parts/plugins/go_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class GoPluginProperties(PluginProperties, frozen=True):

go_buildtags: list[str] = []
go_generate: list[str] = []
go_workspace_use: list[str] = []
sergiusens marked this conversation as resolved.
Show resolved Hide resolved

# part properties required by the plugin
source: str # pyright: ignore[reportGeneralTypeIssues]
Expand Down Expand Up @@ -120,12 +121,22 @@ def get_build_commands(self) -> list[str]:
"""Return a list of commands to run during the build step."""
options = cast(GoPluginProperties, self._options)

# Matches go-use plugin expectation.
workspace_dir = self._part_info._project_info.dirs.parts_dir
workspace = workspace_dir / "work.go"

init_cmds: list[str] = []
if workspace:
init_cmds.append(f"go work use {self._part_info.part_build_dir}")
else:
init_cmds.append("go mod download all")

tags = f"-tags={','.join(options.go_buildtags)}" if options.go_buildtags else ""

generate_cmds: list[str] = [f"go generate {cmd}" for cmd in options.go_generate]

return [
"go mod download all",
*init_cmds,
*generate_cmds,
f'go install -p "{self._part_info.parallel_build_count}" {tags} ./...',
]
83 changes: 83 additions & 0 deletions craft_parts/plugins/go_use_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2020-2021,2024 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""The Go Use plugin."""

import logging
from typing import Literal

from overrides import override

from craft_parts import errors

from .base import Plugin
from .properties import PluginProperties
from .go_plugin import GoPluginEnvironmentValidator

logger = logging.getLogger(__name__)


class GoUsePluginProperties(PluginProperties, frozen=True):
"""The part properties used by the Go Use plugin."""

plugin: Literal["go-use"] = "go-use"

# part properties required by the plugin
source: str # pyright: ignore[reportGeneralTypeIssues]


class GoUsePlugin(Plugin):
"""A plugin to to setup the source into a go workspace.
sergiusens marked this conversation as resolved.
Show resolved Hide resolved

The go plugin requires a go compiler installed on your system. This can
be achieved by adding the appropriate golang package to ``build-packages``,
or to have it installed or built in a different part. In this case, the
name of the part supplying the go compiler must be "go".
"""

properties_class = GoUsePluginProperties
validator_class = GoPluginEnvironmentValidator

@classmethod
def get_out_of_source_build(cls) -> bool:
"""Return whether the plugin performs out-of-source-tree builds."""
return True

@override
def get_build_snaps(self) -> set[str]:
"""Return a set of required snaps to install in the build environment."""
return set()

@override
def get_build_packages(self) -> set[str]:
"""Return a set of required packages to install in the build environment."""
return set()

@override
def get_build_environment(self) -> dict[str, str]:
"""Return a dictionary with the environment to use in the build step."""
return {}

@override
def get_build_commands(self) -> list[str]:
"""Return a list of commands to run during the build step."""
# Set the go workspace directory to live at the root of all parts.
sergiusens marked this conversation as resolved.
Show resolved Hide resolved
workspace_dir = self._part_info._project_info.dirs.parts_dir

return [
f"[ -f '{workspace_dir}' ] || (cd {workspace_dir} && go work init)",
f"go work use {self._part_info.part_src_dir}",
]
2 changes: 2 additions & 0 deletions craft_parts/plugins/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from .dotnet_plugin import DotnetPlugin
from .dump_plugin import DumpPlugin
from .go_plugin import GoPlugin
from .go_use_plugin import GoUsePlugin
from .make_plugin import MakePlugin
from .maven_plugin import MavenPlugin
from .meson_plugin import MesonPlugin
Expand Down Expand Up @@ -54,6 +55,7 @@
"dotnet": DotnetPlugin,
"dump": DumpPlugin,
"go": GoPlugin,
"go-use": GoUsePlugin,
"make": MakePlugin,
"maven": MavenPlugin,
"meson": MesonPlugin,
Expand Down
8 changes: 6 additions & 2 deletions docs/common/craft-parts/reference/plugins/go_plugin.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ How it works

During the build step the plugin performs the following actions:

* Call ``go mod download all`` to find and download all necessary modules;
* If a `go workspace`_ has been setup by use of the :ref:`go-use <craft_parts_go_use_plugin>`
plugin,
call ``go work use <build-dir>`` to add the source for the part to the workspace;
* If not operating in the context of a `go workspace`_, call ``go mod download all``
to find and download all necessary modules;
* Call ``go generate <item>`` for each item in ``go-generate``;
* Call ``go install ./...``, passing the items in ``go-buildtags`` through the
``--tags`` parameter.
Expand Down Expand Up @@ -94,4 +98,4 @@ The following snippet declares a part using the ``go`` plugin. It uses the stabl
.. _Build tags: https://pkg.go.dev/cmd/go#hdr-Build_constraints
.. _Go: https://go.dev/
.. _go generate: https://go.dev/blog/generate

.. _go workspace: https://go.dev/blog/get-familiar-with-workspaces
71 changes: 71 additions & 0 deletions docs/common/craft-parts/reference/plugins/go_use_plugin.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
.. _craft_parts_go_use_plugin:

Go Use plugin
=============

The Go plugin allows for setting up a `go workspace`_ for `Go`_ modules. It is
sergiusens marked this conversation as resolved.
Show resolved Hide resolved
a companion plugin meant to be used with the :ref:`Go plugin <craft_parts_go_plugin>`.
Use of this plugin sets up ``go.work`` and affects all parts.

Keywords
--------

There are no additional keywords to the the common :ref:`plugin <part-properties-plugin>`
and :ref:`sources <part-properties-sources>` keywords.

.. _go-use-details-begin:

Dependencies
------------

The Go plugin needs the ``go`` executable to build Go programs but does not
provision it by itself, to allow flexibility in the choice of compiler version.

Common means of providing ``go`` are:

* The ``golang`` Ubuntu package, declared as a ``build-package``.
* The ``go`` snap, declared as a ``build-snap`` from the desired channel.

Another alternative is to define another part with the name ``go-deps``, and
declare that the part using the ``go`` plugin comes :ref:`after <after>` the
``go-deps`` part. In this case, the plugin will assume that this new part will
stage the ``go`` executable to be used in the build step. This can be useful,
for example, in cases where a specific, unreleased version of ``go`` is desired
but unavailable as a snap or an Ubuntu package.

.. _go-use-details-end:

How it works
------------

During the build step the plugin performs the following actions:

* Setup a `go workspace`_ if ``go.work`` has not been setup;
* Call ``go work use <source-dir>`` to add the source for the part to the workspace;

Examples
--------

The following snippet declares a parts named ``go-flags`` using the ``go-use`` plugin and
a ``hello`` part that declares this ``go-flags``` in its ``go.mod`` using the ``go`` plugin.
Correct ordering is achieved with the use of the ``after`` keyword in the ``hello`` part.

.. code-block:: yaml

parts:
go-flags:
source: https://github.com/jessevdk/go-flags.git
plugin: go-use
hello:
build-snaps:
- go/1.22/stable
plugin: go
source: .
after:
- go-flags


.. _Build tags: https://pkg.go.dev/cmd/go#hdr-Build_constraints
.. _Go: https://go.dev/
.. _go generate: https://go.dev/blog/generate
.. _go workspace: https://go.dev/blog/get-familiar-with-workspaces
1 change: 1 addition & 0 deletions docs/reference/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ lifecycle.
/common/craft-parts/reference/plugins/cmake_plugin.rst
/common/craft-parts/reference/plugins/dump_plugin.rst
/common/craft-parts/reference/plugins/go_plugin.rst
/common/craft-parts/reference/plugins/go_use_plugin.rst
/common/craft-parts/reference/plugins/make_plugin.rst
/common/craft-parts/reference/plugins/maven_plugin.rst
/common/craft-parts/reference/plugins/meson_plugin.rst
Expand Down
38 changes: 38 additions & 0 deletions tests/integration/plugins/test_go.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,41 @@ def test_go_generate(new_dir, partitions):
output = subprocess.check_output([str(binary)], text=True)
# This is the expected output that "gen/generator.go" sets in "main.go"
assert output == "This is a generated line\n"


def test_go_workspace_use(new_dir, partitions):
source_location = Path(__file__).parent / "test_go_workspace"

parts_yaml = textwrap.dedent(
f"""
parts:
go-flags:
source: https://github.com/jessevdk/go-flags.git
plugin: go-use
hello:
after:
- go-flags
plugin: go
source: {source_location}
build-environment:
- GO111MODULE: "on"
"""
)
parts = yaml.safe_load(parts_yaml)
lf = LifecycleManager(
parts,
application_name="test_go",
cache_dir=new_dir,
work_dir=new_dir,
partitions=partitions,
)
actions = lf.plan(Step.PRIME)

with lf.action_executor() as ctx:
ctx.execute(actions)

binary = Path(lf.project_info.prime_dir, "bin", "workspace")
assert binary.is_file()

output = subprocess.check_output([str(binary), "--say=hello"], text=True)
assert output == "hello\n"
7 changes: 7 additions & 0 deletions tests/integration/plugins/test_go_workspace/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module example/workspace

go 1.23

require github.com/jessevdk/go-flags v1.6.1

require golang.org/x/sys v0.25.0 // indirect
4 changes: 4 additions & 0 deletions tests/integration/plugins/test_go_workspace/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
30 changes: 30 additions & 0 deletions tests/integration/plugins/test_go_workspace/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package main

import (
"fmt"
"github.com/jessevdk/go-flags"
"os"
)

type Options struct {
Say string `long:"say" description:"What to say" optional:"yes" default:"Hello world"`
}

var options Options

var parser = flags.NewParser(&options, flags.Default)

func main() {
if _, err := parser.Parse(); err != nil {
switch flagsErr := err.(type) {
case flags.ErrorType:
if flagsErr == flags.ErrHelp {
os.Exit(0)
}
os.Exit(1)
default:
os.Exit(1)
}
}
fmt.Printf("%s\n", options.Say)
}
17 changes: 17 additions & 0 deletions tests/unit/plugins/test_go_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,20 @@ def test_get_build_commands_go_generate(part_info):
"go generate -x b",
'go install -p "1" ./...',
]


def test_get_build_commands_go_workspace_use(part_info):
properties = GoPlugin.properties_class.unmarshal({"source": "."})
plugin = GoPlugin(properties=properties, part_info=part_info)

workspace_dir = plugin._part_info._project_info.dirs.parts_dir
workspace = workspace_dir / "work.go"
workspace_dir.mkdir(parents=True)
workspace.touch()

assert plugin.get_build_commands() == [
f"go work use {plugin._part_info.part_build_dir}",
"go work use go-flags",
"go work use go-cmp",
'go install -p "1" ./...',
]
Loading