Skip to content

Commit

Permalink
debian fedora: add support for APT/Yum repository
Browse files Browse the repository at this point in the history
"dnf module {enable,disable}" is also supported.
  • Loading branch information
kou committed Jan 12, 2025
1 parent b9dafba commit 8c39b78
Show file tree
Hide file tree
Showing 13 changed files with 531 additions and 71 deletions.
123 changes: 122 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ Gem::Specification.new do |spec|
#
# On Ubuntu 24.04:
# https://packages.groonga.org/%{distribution}/groonga-apt-source-latest-%{code_name}.deb ->
# https://packages.groonga.org/ubuntu/groonga-apt-source-latest-nobole.deb
# https://packages.groonga.org/ubuntu/groonga-apt-source-latest-noble.deb
spec.requirements << "system: groonga: debian: https://packages.groonga.org/%{distribution}/groonga-apt-source-latest-%{code_name}.deb"
# Install libgroonga-dev from the registered repository.
spec.requirements << "system: groonga: debian: libgroonga-dev"
Expand Down Expand Up @@ -207,6 +207,127 @@ Gem::Specification.new do |spec|
end
```

### Install repositories

You can install APT/Yum repositories by specifying metadata.

You need to specify multiple metadata for one repository. So you need
to use multiple `spec.requirements` for one repository. Here is the
syntax for one repository:

```ruby
spec.requirements << "system: #{package}: #{platform}: repository: #{key1}: #{value1}"
spec.requirements << "system: #{package}: #{platform}: repository: #{key2}: #{value2}"
# ...
```

You must specify at least `id` as `key`. For example:

```
spec.requirements << "system: libpq: debian: repository: id: pgdg"
```

You can start another repository metadata by starting `id` metadata
for another repository:

```ruby
spec.requirements << "system: #{package}: #{platform}: repository: id: repository1
spec.requirements << "system: #{package}: #{platform}: repository: #{key1_1}: #{value1_1}"
spec.requirements << "system: #{package}: #{platform}: repository: #{key1_2}: #{value1_2}"
# ...
spec.requirements << "system: #{package}: #{platform}: repository: id: repository2
spec.requirements << "system: #{package}: #{platform}: repository: #{key2_1}: #{value2_1}"
spec.requirements << "system: #{package}: #{platform}: repository: #{key2_2}: #{value2_2}"
# ...
```

Here are metadata for a APT repository:

* `compoennts`: Optional. The default is `main`.
* `signed-by`: Optional. The URL of armored keyring that is used for
signing this repository.
* `suites`: Optional. The default is `%{code_name}`.
* `types`: Optional. The default is `deb`.
* `uris`: Required. The URLs that provide this repository.

See also: https://wiki.debian.org/SourcesList

Here are metadata for a Yum repository:

* `baseurl`: Required. The base URL that provides this repository.
* `gpgcheck`: Optional. No default.
* `gpgkey`: Optional. The URL of GPG key that is used for signing this
repository.
* `name`: Optional. The name of this repository.

See also: TODO: Is there any URL that describes the specification of
`.repo` file?

You can use placeholder for metadata values with `%{KEY}` format.

Here are available placeholders:

`debian` family platforms:

* `distribution`: The `ID` value in `/etc/os-release`. It's `debian`,
`ubuntu` and so on.
* `code_name`: The `VERSION_CODENAME` value in `/etc/os-release`. It's
`bookworm`, `noble` and so on.
* `version`: The `VERSION_ID` value in `/etc/os-release`. It's `12`,
`24.04` and so on.

`fedora` family platforms:

* `distribution`: The `ID` value in `/etc/os-release`. It's `fedora`,
`rhel`, `almalinux` and so on.
* `major_version`: The major part of `VERSION_ID` value in
`/etc/os-release`. It's `41`, `9` and so on.
* `version`: The `VERSION_ID` value in `/etc/os-release`. It's `41`,
`9.5` and so on.

Here is an example that uses this feature for adding a new repository:

```ruby
Gem::Specification.new do |spec|
# ...

# Install PostgreSQL's APT repository on Debian family platforms.
#
# %{code_name} is placeholders.
#
# On Debian GNU/Linux bookworm:
# %{code_name}-pgdg ->
# bookworm-pgdg
#
# On Ubuntu 24.04:
# %{code_name}-pgdg ->
# noble-pgdg
spec.requirements << "system: libpq: debian: repository: id: pgdg"
spec.requirements << "system: libpq: debian: repository: uris: https://apt.postgresql.org/pub/repos/apt"
spec.requirements << "system: libpq: debian: repository: signed-by: https://www.postgresql.org/media/keys/ACCC4CF8.asc"
spec.requirements << "system: libpq: debian: repository: suites: %{code_name}-pgdg"
spec.requirements << "system: libpq: debian: repository: components: main"
# Install libpq-dev from the registered repository.
spec.requirements << "system: libpq: debian: libpq-dev"

# Install PostgreSQL's Yum repository on RHEL family platforms:
spec.requirements << "system: libpq: rhel: repository: id: pgdg17"
spec.requirements << "system: libpq: rhel: repository: name: PostgreSQL 17 $releasever - $basearch"
spec.requirements << "system: libpq: rhel: repository: baseurl: https://download.postgresql.org/pub/repos/yum/17/redhat/rhel-$releasever-$basearch"
spec.requirements << "system: libpq: rhel: repository: gpgcheck: 1"
spec.requirements << "system: libpq: rhel: repository: gpgkey: https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-RHEL"
# You can disable built-in "postgresql" module by "module: disable:
# postgresql".
spec.requirements << "system: libpq: rhel: module: disable: postgresql"
# Install postgresql17-devel from the registered repository. But
# users can't find "libpq.pc" provided by postgresql17-devel without
# PKG_CONFIG_PATH=/usr/pgsql-17/lib/pkgconfig ...
spec.requirements << "system: libpq: rhel: postgresql17-devel"

# ...
end
```

## Configurations

### Opt-out
Expand Down
19 changes: 15 additions & 4 deletions dockerfiles/run-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -80,26 +80,37 @@ group_end

for test_gem in "${test_gems[@]}"; do
pushd ${test_gem}
group_begin "Test: $(basename ${test_gem}): Prepare"
gem_name=$(basename ${test_gem})
group_begin "Test: ${gem_name}: Prepare"
gem build *.gemspec
case "${gem_name}" in
dummy-postgresql-*)
# This is only for RHEL. PGDG's Yum repository uses
# /usr/pgsql-${VERSION}/lib/pkgconfig/ not /usr/lib64/pkgconfig/
# for libpq.pc.
postgresql_version=${gem_name#dummy-postgresql-}
postgresql_pkg_config_path=/usr/pgsql-${postgresql_version}/lib/pkgconfig
export PKG_CONFIG_PATH="${postgresql_pkg_config_path}${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}"
;;
esac
group_end
# Must be failed
for disabled_value in 0 no NO false FALSE; do
group_begin "Test: $(basename ${test_gem}): Disable by env: ${disabled_value}"
group_begin "Test: ${gem_name}: Disable by env: ${disabled_value}"
if RUBYGEMS_REQUIREMENTS_SYSTEM=${disabled_value} \
gem install "${gem_install_options[@]}" ./*.gem; then
exit 1
fi
group_end
done
group_begin "Test: $(basename ${test_gem}): Disable by configuration"
group_begin "Test: ${gem_name}: Disable by configuration"
# Must be failed
if gem install "${gem_install_options[@]}" \
--config-file=${disable_gemrc} ./*.gem; then
exit 1
fi
group_end
group_begin "Test: $(basename ${test_gem}): Default"
group_begin "Test: ${gem_name}: Default"
# Must be succeeded
gem install "${gem_install_options[@]}" ./*.gem
group_end
Expand Down
51 changes: 2 additions & 49 deletions lib/rubygems-requirements-system/installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ def initialize(gemspec)
def install
return true unless enabled?

requirements = parse_requirements(@gemspec.requirements)
parser = RequirementsParser.new(@gemspec.requirements, @platform)
requirements = parser.parse
requirements.all? do |requirement|
next true if requirement.satisfied?
@platform.install(requirement)
Expand All @@ -58,53 +59,5 @@ def enabled?

true
end

def parse_requirements(gemspec_requirements)
all_packages_set = {}
requirements = {}
gemspec_requirements.each do |gemspec_requirement|
components = gemspec_requirement.split(/: +/, 4)
next unless components.size == 4

id, raw_packages, platform, system_package = components
next unless id == "system"

packages = parse_packages(raw_packages)
next if packages.empty?

all_packages_set[packages] = true

next unless @platform.target?(platform)
requirements[packages] ||= []
requirements[packages] << system_package
end
(all_packages_set.keys - requirements.keys).each do |not_used_packages|
system_packages = @platform.default_system_packages(not_used_packages)
next if system_packages.nil?
requirements[not_used_packages] = system_packages
end
requirements.collect do |packages, system_packages|
Requirement.new(packages, system_packages)
end
end

def parse_packages(raw_packages)
packages = raw_packages.split(/\s*\|\s*/).collect do |raw_package|
Package.parse(raw_package)
end
# Ignore this requirement if any invalid package is included.
# We must not report an error for this because
# Gem::Specification#requirements is a free form
# configuration. So there are configuration values that use
# "system: ..." but not for this plugin. We can report a
# warning instead.
packages.each do |package|
unless package.valid?
# TODO: Report a warning
return []
end
end
packages
end
end
end
2 changes: 1 addition & 1 deletion lib/rubygems-requirements-system/os-release.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def id
end

def id_like
(self["ID_LIKE"] || "").split(/ /)
(self["ID_LIKE"] || "").split(/\s+/)
end

def version
Expand Down
21 changes: 18 additions & 3 deletions lib/rubygems-requirements-system/platform/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ def default_system_packages(packages)
nil
end

def valid_system_package?(package)
true
end

def valid_system_repository?(repository)
false
end

def install(requirement)
synchronize do
requirement.system_packages.any? do |package|
Expand Down Expand Up @@ -109,12 +117,19 @@ def install_package(package)
end

def run_command_line(package, action, command_line)
if package.is_a?(SystemRepository)
package_label = "repository(#{package.id})"
else
package_label = package
end
prefix = "requirements: system: #{package_label}: #{action}"
say("#{prefix}: Start")
failed_to_get_super_user_priviledge = false
if have_priviledge?
succeeded = system(*command_line)
else
if sudo
prompt = "[sudo] password for %u to #{action} <#{package}>: "
prompt = "[sudo] password for %u to #{action} <#{package_label}>: "
command_line = [sudo, "-p", prompt, *command_line]
succeeded = system(*command_line)
else
Expand All @@ -127,13 +142,13 @@ def run_command_line(package, action, command_line)
else
result_message = succeeded ? "succeeded" : "failed"
end
say("#{action.capitalize} '#{package}' system package: #{result_message}")
say("#{prefix}: #{result_message}")

unless succeeded
escaped_command_line = command_line.collect do |part|
Shellwords.escape(part)
end
alert_warning("'#{package}' system package is required.")
alert_warning("#{prefix}: Failed.")
alert_warning("Run the following command " +
"to #{action} required system package: " +
escaped_command_line.join(" "))
Expand Down
Loading

0 comments on commit 8c39b78

Please sign in to comment.