diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..12ea855 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,27 @@ +name: Ruby + +on: + push: + branches: + - master + + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + name: Ruby ${{ matrix.ruby }} + strategy: + matrix: + ruby: + - '3.3.5' + + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Run the default task + run: bundle exec rake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3fe11ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +.idea +Gemfile.lock \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..be08bf8 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,55 @@ +AllCops: + TargetRubyVersion: 3.3 + +Style/StringLiterals: + EnforcedStyle: double_quotes + +Style/StringLiteralsInInterpolation: + EnforcedStyle: double_quotes + +Style/FormatStringToken: + EnforcedStyle: template + +## Settings below are meant to be enabled (at least some of them) +## but in order to make rubocop useful first for correcting obvious things, +## they were disabled for the time being. TODO: review + +Style/Documentation: + Enabled: false + +Metrics/MethodLength: + Enabled: false + +Metrics/AbcSize: + Enabled: false + +Metrics/ClassLength: + Enabled: false + +Metrics/PerceivedComplexity: + Enabled: false + +Metrics/CyclomaticComplexity: + Enabled: false + +Naming/MethodParameterName: + Enabled: false + +Layout/LineLength: + Enabled: false + +Metrics/BlockLength: + Enabled: false + +Naming/HeredocDelimiterNaming: + Enabled: false + +Style/IfUnlessModifier: + Enabled: false + +Style/ClassVars: + Enabled: false + +Style/MultilineBlockChain: + Enabled: false + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..78d409e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +## [Unreleased] + +## [0.1.0] - 2024-10-04 + +- Initial release diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cfdec65 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM fedora:41 AS pgpm-rpm + +RUN dnf -y install rpmlint ruby ruby-devel mock git gcc zlib-devel +VOLUME /pgpm +WORKDIR /pgpm \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..7299dc2 --- /dev/null +++ b/Gemfile @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +# Specify your gem's dependencies in pgpm.gemspec +gemspec + +gem "rake", "~> 13.0" + +gem "minitest", "~> 5.16" + +gem "rubocop", "~> 1.21" + +gem "debug", "~> 1.9" + +gem "rbs", "~> 3.6" diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..97f90f7 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 Yurii Rashkovskii + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f2b7f20 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Postgres Package Manager \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..2bf771f --- /dev/null +++ b/Rakefile @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "bundler/gem_tasks" +require "minitest/test_task" + +Minitest::TestTask.create + +require "rubocop/rake_task" + +RuboCop::RakeTask.new + +task default: %i[test rubocop] diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..1225868 --- /dev/null +++ b/bin/console @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "pgpm" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +require "irb" +IRB.start(__FILE__) diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..dce67d8 --- /dev/null +++ b/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/exe/pgpm b/exe/pgpm new file mode 100755 index 0000000..7cc1dde --- /dev/null +++ b/exe/pgpm @@ -0,0 +1,88 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "pgpm" +require "dry/cli" +require "parallel" + +module Pgpm + module CLI + module Commands + extend Dry::CLI::Registry + + class Version < Dry::CLI::Command + desc "Print version" + + def call(*) + puts Pgpm::VERSION + end + end + + class Build < Dry::CLI::Command + desc "Build packages" + + option :os, type: :string, default: Pgpm::OS.auto_detect.name, desc: "OS name" + argument :packages, type: :array, required: true, desc: "Package names" + + def call(packages:, args: nil, os: nil) + _ = args + _ = os + + # puts "There is no build support for OS distribution `#{os}`" + # exit(1) + pkgs = Parallel.flat_map(packages) do |package| + name, version = package.split("@") + version ||= :latest + p = Pgpm::Package[name] + if p.nil? + puts "Package #{name} not found" + exit(1) + end + pkg = p[version] + if pkg.nil? + puts "Package #{name} with version #{version} not found" + exit(1) + end + pkg + end + b = pkgs.reduce(nil) do |c, p| + spec = p.to_rpm_spec + builder = Pgpm::RPM::Builder.new(spec, os: Pgpm::OS::RockyEPEL9.new) + src_builder = builder.source_builder + p = c.nil? ? src_builder : c.and_then(src_builder) + p.and_then(builder.versionless_builder) + end + + srpms = b.call + puts "=============" + pp srpms + puts "============= " + Pgpm::RPM::Builder.builder(srpms, os: Pgpm::OS::RockyEPEL9.new).call + end + end + + class Search < Dry::CLI::Command + argument :query, type: :string, default: ".*", desc: "Search query" + + def call(query:, args: nil) + _ = args + query_regexp = Regexp.new(query, "i") + Parallel.filter_map(Pgpm::Package) do |p| + matches = p.all_searchable_texts.any? do |t| + t =~ query_regexp + end + "#{p.package_name}: #{p.description}" if matches + end.each { |l| puts l } + end + end + + register "version", Version, aliases: ["v", "-v", "--version"] + register "build", Build + register "search", Search + end + end +end + +Pgpm.load_packages(File.join(Dir.pwd, "packages")) +Dry::CLI.new(Pgpm::CLI::Commands).call diff --git a/lib/pgpm.rb b/lib/pgpm.rb new file mode 100644 index 0000000..ff4cfe7 --- /dev/null +++ b/lib/pgpm.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "zeitwerk" +require "pathname" + +class CustomInflector < Zeitwerk::GemInflector + def camelize(basename, _abspath) + # Specify your custom logic here + # This tells Zeitwerk that 'rpm' should be 'RPM' and 'os' should be 'OS' + case basename + when "rpm" + "RPM" + when "os" + "OS" + when "pgxn" + "PGXN" + else + super + end + end +end + +loader = Zeitwerk::Loader.for_gem +loader.inflector = CustomInflector.new(__FILE__) +loader.enable_reloading +loader.setup + +define_method(:reload!) do + loader.reload + loader.eager_load +end + +define_method(:load_packages) do |path = nil| + path ||= Pathname(File.dirname(__FILE__)).join("..", "packages") + loader.push_dir(path) + reload! +end + +module Pgpm +end diff --git a/lib/pgpm/aspects/inheritance_tracker.rb b/lib/pgpm/aspects/inheritance_tracker.rb new file mode 100644 index 0000000..9431c43 --- /dev/null +++ b/lib/pgpm/aspects/inheritance_tracker.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Pgpm + module Aspects + module InheritanceTracker + module ClassMethods + def all_subclasses + subclasses + subclasses.flat_map(&:all_subclasses) + end + end + + def self.included(base_class) + base_class.extend(ClassMethods) + end + end + end +end diff --git a/lib/pgpm/cache.rb b/lib/pgpm/cache.rb new file mode 100644 index 0000000..c9fc299 --- /dev/null +++ b/lib/pgpm/cache.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require "xdg" + +module Pgpm + class Cache + def self.directory + XDG.new.cache_home.join("postgres.pm") + end + end +end diff --git a/lib/pgpm/commands.rb b/lib/pgpm/commands.rb new file mode 100644 index 0000000..4cc3cc3 --- /dev/null +++ b/lib/pgpm/commands.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "lspace" + +module Pgpm + module Commands + class Abstract + def to_s + raise "abstract implementation" + end + end + + class Make < Abstract + def initialize(*args) + @args = args + super() + end + + def to_s + command = "make" + command += " %{?_smp_mflags}" if Pgpm::OS.in_scope.is_a?(Pgpm::OS::RedHat) + command += " #{@args.join(" ")}" unless @args.empty? + command + end + end + end +end diff --git a/lib/pgpm/on_demand_file.rb b/lib/pgpm/on_demand_file.rb new file mode 100644 index 0000000..340ae07 --- /dev/null +++ b/lib/pgpm/on_demand_file.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Pgpm + class OnDemandFile + attr_reader :name + + def initialize(name, proc) + @name = name + @proc = proc + end + + private + + def respond_to_missing?(symbol) + @io ||= @proc.call + @io.respond_to?(symbol) + end + + def method_missing(name, *args) + @io ||= @proc.call + @io.send(name, *args) + end + end +end diff --git a/lib/pgpm/os.rb b/lib/pgpm/os.rb new file mode 100644 index 0000000..f140c10 --- /dev/null +++ b/lib/pgpm/os.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Pgpm + module OS + class Base + attr_reader :arch + + include Pgpm::Aspects::InheritanceTracker + + def self.name + "unknown" + end + + def name + self.class.name + end + + def self.builder + nil + end + + def with_scope(&block) + LSpace.with(pgpm_target_operating_system: self) do + block.yield + end + end + end + + def self.auto_detect + return unless RUBY_PLATFORM =~ /linux$/ + + Pgpm::OS::Linux.auto_detect + end + + def self.in_scope + LSpace[:pgpm_target_operating_system] + end + end +end diff --git a/lib/pgpm/os/linux.rb b/lib/pgpm/os/linux.rb new file mode 100644 index 0000000..a821227 --- /dev/null +++ b/lib/pgpm/os/linux.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Pgpm + module OS + class Linux < Pgpm::OS::Unix + def self.auto_detect + return unless File.exist?("/etc/redhat-release") + + RedHat.auto_detect + end + + def self.name + "linux" + end + end + end +end diff --git a/lib/pgpm/os/red_hat.rb b/lib/pgpm/os/red_hat.rb new file mode 100644 index 0000000..898fe18 --- /dev/null +++ b/lib/pgpm/os/red_hat.rb @@ -0,0 +1,399 @@ +# frozen_string_literal: true + +require "rbconfig" + +module Pgpm + module OS + class RedHat < Pgpm::OS::Linux + def self.auto_detect + new # TODO: distinguish between flavors of RedHat + end + + def self.name + "redhat" + end + end + + class RockyEPEL9 < Pgpm::OS::RedHat + def self.name + "rocky+epel-9" + end + + def self.builder + Pgpm::RPM::Builder + end + + def initialize(arch: nil) + @arch = arch || RbConfig::CONFIG["host_cpu"] + super() + end + + def mock_config(_root = "rocky+epel-9-#{@arch}") + <<~EOF + include('rocky+epel-9-#{@arch}.cfg') + config_opts['root'] = 'rocky+epel+pgdg-9-aarch64' + + config_opts['description'] = 'Rocky Linux EPEL 9' + config_opts['dnf.conf'] += """ + + ######################################################### + # PGDG Red Hat Enterprise Linux / Rocky repositories # + ######################################################### + + # PGDG Red Hat Enterprise Linux / Rocky stable common repository for all PostgreSQL versions + + [pgdg-common] + name=PostgreSQL common RPMs for RHEL / Rocky / AlmaLinux $releasever - $basearch + baseurl=https://download.postgresql.org/pub/repos/yum/common/redhat/rhel-$releasever-$basearch + enabled=1 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + # Red Hat recently breaks compatibility between 9.n and 9.n+1. PGDG repo is + # affected with the LLVM repo. This is a band aid repo for the llvmjit users + # whose installations cannot be updated. + + [pgdg-rocky9-sysupdates] + name=PostgreSQL Supplementary ucommon RPMs for RHEL / Rocky / AlmaLinux $releasever - $basearch + baseurl=https://download.postgresql.org/pub/repos/yum/common/pgdg-rocky9-sysupdates/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + # We provide extra package to support some RPMs in the PostgreSQL RPM repo, like + # consul, haproxy, etc. + + [pgdg-rhel9-extras] + name=Extra packages to support some RPMs in the PostgreSQL RPM repo for RHEL / Rocky / AlmaLinux $releasever - $basearch + baseurl=https://download.postgresql.org/pub/repos/yum/common/pgdg-rhel$releasever-extras/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + # PGDG Red Hat Enterprise Linux / Rocky stable repositories: + + [pgdg17] + name=PostgreSQL 17 for RHEL / Rocky / AlmaLinux $releasever - $basearch + baseurl=https://download.postgresql.org/pub/repos/yum/17/redhat/rhel-$releasever-$basearch + enabled=1 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg16] + name=PostgreSQL 16 for RHEL / Rocky / AlmaLinux $releasever - $basearch + baseurl=https://download.postgresql.org/pub/repos/yum/16/redhat/rhel-$releasever-$basearch + enabled=1 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg15] + name=PostgreSQL 15 for RHEL / Rocky / AlmaLinux $releasever - $basearch + baseurl=https://download.postgresql.org/pub/repos/yum/15/redhat/rhel-$releasever-$basearch + enabled=1 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg14] + name=PostgreSQL 14 for RHEL / Rocky / AlmaLinux $releasever - $basearch + baseurl=https://download.postgresql.org/pub/repos/yum/14/redhat/rhel-$releasever-$basearch + enabled=1 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg13] + name=PostgreSQL 13 for RHEL / Rocky / AlmaLinux $releasever - $basearch + baseurl=https://download.postgresql.org/pub/repos/yum/13/redhat/rhel-$releasever-$basearch + enabled=1 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg12] + name=PostgreSQL 12 for RHEL / Rocky / AlmaLinux $releasever - $basearch + baseurl=https://download.postgresql.org/pub/repos/yum/12/redhat/rhel-$releasever-$basearch + enabled=1 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + # PGDG RHEL / Rocky / AlmaLinux Updates Testing common repositories. + + [pgdg-common-testing] + name=PostgreSQL common testing RPMs for RHEL / Rocky / AlmaLinux $releasever - $basearch + baseurl=https://download.postgresql.org/pub/repos/yum/testing/common/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + # PGDG RHEL / Rocky / AlmaLinux Updates Testing repositories. (These packages should not be used in production) + # Available for 12 and above. + + [pgdg17-updates-testing] + name=PostgreSQL 17 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Updates testing + baseurl=https://download.postgresql.org/pub/repos/yum/testing/17/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=0 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg16-updates-testing] + name=PostgreSQL 16 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Updates testing + baseurl=https://download.postgresql.org/pub/repos/yum/testing/16/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg15-updates-testing] + name=PostgreSQL 15 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Updates testing + baseurl=https://download.postgresql.org/pub/repos/yum/testing/15/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg14-updates-testing] + name=PostgreSQL 14 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Updates testing + baseurl=https://download.postgresql.org/pub/repos/yum/testing/14/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg13-updates-testing] + name=PostgreSQL 13 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Updates testing + baseurl=https://download.postgresql.org/pub/repos/yum/testing/13/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg12-updates-testing] + name=PostgreSQL 12 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Updates testing + baseurl=https://download.postgresql.org/pub/repos/yum/testing/12/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + # PGDG Red Hat Enterprise Linux / Rocky SRPM testing common repository + + [pgdg-source-common] + name=PostgreSQL 12 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Source + baseurl=https://download.postgresql.org/pub/repos/yum/srpms/common/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + # PGDG RHEL / Rocky / AlmaLinux testing common SRPM repository for all PostgreSQL versions + + [pgdg-common-srpm-testing] + name=PostgreSQL common testing SRPMs for RHEL / Rocky / AlmaLinux $releasever - $basearch + baseurl=https://download.postgresql.org/pub/repos/yum/srpms/testing/common/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + # PGDG Source RPMs (SRPM), and their testing repositories: + + [pgdg17-source-updates-testing] + name=PostgreSQL 17 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Source updates testing + baseurl=https://download.postgresql.org/pub/repos/yum/srpms/testing/17/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=0 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg16-source] + name=PostgreSQL 16 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Source + baseurl=https://download.postgresql.org/pub/repos/yum/srpms/16/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg16-source-updates-testing] + name=PostgreSQL 16 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Source updates testing + baseurl=https://download.postgresql.org/pub/repos/yum/srpms/testing/16/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg15-source] + name=PostgreSQL 15 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Source + baseurl=https://download.postgresql.org/pub/repos/yum/srpms/15/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg15-source-updates-testing] + name=PostgreSQL 15 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Source updates testing + baseurl=https://download.postgresql.org/pub/repos/yum/srpms/testing/15/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg14-source] + name=PostgreSQL 14 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Source + baseurl=https://download.postgresql.org/pub/repos/yum/srpms/14/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg14-source-updates-testing] + name=PostgreSQL 14 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Source updates testing + baseurl=https://download.postgresql.org/pub/repos/yum/srpms/testing/14/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg13-source] + name=PostgreSQL 13 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Source + baseurl=https://download.postgresql.org/pub/repos/yum/srpms/13/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg13-source-updates-testing] + name=PostgreSQL 13 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Source updates testing + baseurl=https://download.postgresql.org/pub/repos/yum/srpms/testing/13/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg12-source] + name=PostgreSQL 12 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Source + baseurl=https://download.postgresql.org/pub/repos/yum/srpms/12/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg12-source-updates-testing] + name=PostgreSQL 12 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Source update testing + baseurl=https://download.postgresql.org/pub/repos/yum/srpms/testing/12/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + # Debuginfo/debugsource packages for stable repos + + [pgdg16-debuginfo] + name=PostgreSQL 16 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Debuginfo + baseurl=https://dnf-debuginfo.postgresql.org/debug/16/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg15-debuginfo] + name=PostgreSQL 15 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Debuginfo + baseurl=https://dnf-debuginfo.postgresql.org/debug/15/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg14-debuginfo] + name=PostgreSQL 14 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Debuginfo + baseurl=https://dnf-debuginfo.postgresql.org/debug/14/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg13-debuginfo] + name=PostgreSQL 13 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Debuginfo + baseurl=https://dnf-debuginfo.postgresql.org/debug/13/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg12-debuginfo] + name=PostgreSQL 12 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Debuginfo + baseurl=https://dnf-debuginfo.postgresql.org/debug/12/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + # Debuginfo/debugsource packages for testing repos + # Available for 12 and above. + + [pgdg17-updates-testing-debuginfo] + name=PostgreSQL 17 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Debuginfo + baseurl=https://dnf-debuginfo.postgresql.org/testing/debug/17/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=0 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg16-updates-testing-debuginfo] + name=PostgreSQL 16 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Debuginfo + baseurl=https://dnf-debuginfo.postgresql.org/testing/debug/16/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=0 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg15-updates-testing-debuginfo] + name=PostgreSQL 15 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Debuginfo + baseurl=https://dnf-debuginfo.postgresql.org/testing/debug/15/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg14-updates-testing-debuginfo] + name=PostgreSQL 14 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Debuginfo + baseurl=https://dnf-debuginfo.postgresql.org/testing/debug/14/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg13-updates-testing-debuginfo] + name=PostgreSQL 13 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Debuginfo + baseurl=https://dnf-debuginfo.postgresql.org/testing/debug/13/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + + [pgdg12-updates-testing-debuginfo] + name=PostgreSQL 12 for RHEL / Rocky / AlmaLinux $releasever - $basearch - Debuginfo + baseurl=https://dnf-debuginfo.postgresql.org/testing/debug/12/redhat/rhel-$releasever-$basearch + enabled=0 + gpgcheck=1 + gpgkey=https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL + repo_gpgcheck = 1 + """ + + #config_opts['dnf_install_command'] += ' https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm postgresql16 postgresql16-devel' + config_opts['chroot_setup_cmd'] = 'install @development' + # https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-aarch64/pgdg-redhat-repo-latest.noarch.rpm' + + EOF + end + end + end +end diff --git a/lib/pgpm/os/unix.rb b/lib/pgpm/os/unix.rb new file mode 100644 index 0000000..d92a01e --- /dev/null +++ b/lib/pgpm/os/unix.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Pgpm + module OS + class Unix < Pgpm::OS::Base + def self.name + "unix" + end + end + end +end diff --git a/lib/pgpm/package.rb b/lib/pgpm/package.rb new file mode 100644 index 0000000..205cedb --- /dev/null +++ b/lib/pgpm/package.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require "dry/inflector" + +module Pgpm + class Package + include Pgpm::Aspects::InheritanceTracker + include AbstractPackage + include Sources + include Naming + include Metadata + include Dependencies + include Git + include GitHub + include PGXN + include Versioning + include Subscripting + include Enumerating + include Building + include Initialization + include Packaging + + def inspect + "#<#{self.class}:#{self.class.package_name} #{version}>" + end + + abstract_package + end +end diff --git a/lib/pgpm/package/abstract_package.rb b/lib/pgpm/package/abstract_package.rb new file mode 100644 index 0000000..9e55d71 --- /dev/null +++ b/lib/pgpm/package/abstract_package.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Pgpm + class Package + module AbstractPackage + module ClassMethods + def abstract_package? + @is_abstract_package || false + end + + protected + + def abstract_package + @is_abstract_package = true + end + end + + def self.included(base_class) + base_class.extend(ClassMethods) + end + end + end +end diff --git a/lib/pgpm/package/building.rb b/lib/pgpm/package/building.rb new file mode 100644 index 0000000..48ce85e --- /dev/null +++ b/lib/pgpm/package/building.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Pgpm + class Package + module Building + def configure_steps + [] + end + + def build_steps + return [Pgpm::Commands::Make.new] if makefile_present? + + [] + end + + def install_steps + return [Pgpm::Commands::Make.new("install", "DESTDIR=$PGPM_BUILDROOT")] if makefile_present? + + [] + end + + def makefile_present? + !Dir.glob(%w[Makefile GNUmakefile makefile], base: source.to_s).empty? + end + + def source_url_directory_name + nil + end + end + end +end diff --git a/lib/pgpm/package/dependencies.rb b/lib/pgpm/package/dependencies.rb new file mode 100644 index 0000000..747e694 --- /dev/null +++ b/lib/pgpm/package/dependencies.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Pgpm + class Package + module Dependencies + def build_dependencies + return ["gcc"] if c_files_present? + + [] + end + + def dependencies + [] + end + + def c_files_present? + Dir.glob("*.c", base: source).any? + end + end + end +end diff --git a/lib/pgpm/package/enumerating.rb b/lib/pgpm/package/enumerating.rb new file mode 100644 index 0000000..7e864d0 --- /dev/null +++ b/lib/pgpm/package/enumerating.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Pgpm + class Package + module Enumerating + def self.included(base_class) + class << base_class + include Enumerable + def each(&block) + if self == Pgpm::Package + all_subclasses.each(&block) + else + package_versions.map { |v| new(v) }.each(&block) + end + end + end + end + end + end +end diff --git a/lib/pgpm/package/git.rb b/lib/pgpm/package/git.rb new file mode 100644 index 0000000..0f16cbe --- /dev/null +++ b/lib/pgpm/package/git.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require "git" + +module Pgpm + class Package + module Git + Config = Data.define(:url, :download_version_tags) + + module Methods + def package_versions + if !git_config.download_version_tags + super + else + @tags ||= + ::Git.ls_remote(git_config.url)["tags"].keys + .filter { |key| !key.match?(/.+\^{}$/) } + versions = @tags.map { |tag| tag.gsub(/^v/, "") } + @tag_versions = Hash[@tags.zip(versions)] + @version_tags = Hash[versions.zip(@tags)] + versions + end + end + end + + def self.included(base_class) + base_class.class_eval do + def version_git_tag + self.class.package_versions if self.class.instance_variable_get(:@version_tags).nil? + version_tags = self.class.instance_variable_get(:@version_tags) || {} + version_tags[version] + end + + def version_git_commit + nil + end + + def source + directory = Pgpm::Cache.directory.join(name, version) + tag = version_git_tag + commit = version_git_commit + directory = Pgpm::Cache.directory.join(name, commit) if commit + if File.directory?(directory) && File.directory?(directory.join(".git")) + directory + elsif File.directory?(directory) + raise "Unexpected non-git directory #{directory}" + else + if tag + ::Git.clone(self.class.git_config.url, directory, depth: 1, branch: version_git_tag) + elsif commit + g = ::Git.clone(self.class.git_config.url, directory) + g.checkout("checkout-#{commit}", new_branch: true, start_point: commit) + else + ::Git.clone(self.class.git_config.url, directory, depth: 1) + end + directory + end + end + end + + class << base_class + @git_config = nil + attr_reader :git_config + + def git(url, download_version_tags: true) + @git_config = Config.new(url:, download_version_tags:) + extend Methods + end + end + end + end + end +end diff --git a/lib/pgpm/package/git_hub.rb b/lib/pgpm/package/git_hub.rb new file mode 100644 index 0000000..accd8da --- /dev/null +++ b/lib/pgpm/package/git_hub.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require "open-uri" + +module Pgpm + class Package + module GitHub + Config = Data.define(:name, :download_version_tags) + + def sources + commit = version_git_tag || version_git_commit + [Pgpm::OnDemandFile.new("#{version}.tar.gz", lambda { + URI.open("https://github.com/#{self.class.github_config.name}/archive/#{commit}.tar.gz") + })] + super + end + + module ClassMethods + attr_reader :github_config + + def github(name, download_version_tags: true) + @github_config = Config.new(name:, download_version_tags:) + include Pgpm::Package::Git + git "https://github.com/#{@github_config.name}", download_version_tags: + end + end + + def self.included(base_class) + base_class.extend(ClassMethods) + end + end + end +end diff --git a/lib/pgpm/package/initialization.rb b/lib/pgpm/package/initialization.rb new file mode 100644 index 0000000..5a1c424 --- /dev/null +++ b/lib/pgpm/package/initialization.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Pgpm + class Package + module Initialization + def initialize(version) + @version = version + end + end + end +end diff --git a/lib/pgpm/package/metadata.rb b/lib/pgpm/package/metadata.rb new file mode 100644 index 0000000..6f46e3b --- /dev/null +++ b/lib/pgpm/package/metadata.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Pgpm + class Package + module Metadata + def summary + self.class.summary + end + + def description + self.class.description + end + + def all_searchable_texts + self.class.all_searchable_texts + end + + def license + self.class.license + end + + module ClassMethods + def summary + "TODO: Summary" + end + + def description + "TODO: Description" + end + + def license + "TODO: License" + end + + def all_searchable_texts + [package_name, summary, description] + end + end + + def self.included(base_class) + base_class.extend(ClassMethods) + end + end + end +end diff --git a/lib/pgpm/package/naming.rb b/lib/pgpm/package/naming.rb new file mode 100644 index 0000000..603fc82 --- /dev/null +++ b/lib/pgpm/package/naming.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Pgpm + class Package + module Naming + module ClassMethods + def package_name + class_name = to_s.split("::").last + @name || Dry::Inflector.new.underscore(class_name) + end + + protected + + def name(name) + @name = name + end + end + + def self.included(base_class) + base_class.extend(ClassMethods) + end + + def name + self.class.package_name + end + + def extension_name + name + end + end + end +end diff --git a/lib/pgpm/package/packaging.rb b/lib/pgpm/package/packaging.rb new file mode 100644 index 0000000..cabcdc8 --- /dev/null +++ b/lib/pgpm/package/packaging.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Pgpm + class Package + module Packaging + def to_rpm_spec(**opts) + Pgpm::RPM::Spec.new(self, **opts) + end + end + end +end diff --git a/lib/pgpm/package/pgxn.rb b/lib/pgpm/package/pgxn.rb new file mode 100644 index 0000000..f12d854 --- /dev/null +++ b/lib/pgpm/package/pgxn.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require "oj" + +module Pgpm + class Package + module PGXN + def provides_pgxn_meta_json? + File.directory?(source) && File.file?(pgxn_meta_json_path) + end + + def pgxn_meta_json + @pgxn_meta_json ||= Oj.load(File.read(pgxn_meta_json_path)) + end + + def pgxn_meta_json_path + source.join("META.json") + end + + def extension_name + if provides_pgxn_meta_json? + pgxn_meta_json["name"] + else + super + end + end + + def summary + if provides_pgxn_meta_json? + pgxn_meta_json["abstract"] + else + super + end + end + + def description + if provides_pgxn_meta_json? + pgxn_meta_json["description"] + else + super + end + end + + def license + if provides_pgxn_meta_json? + lic = pgxn_meta_json["license"] + case lic + when Hash + lic.keys.join(" or ") + when Array + lic.join(" or ") + when String + lic + end + else + super + end + end + + module ClassMethods + def extension_name + self[:latest].extension_name + end + + def description + self[:latest].description + end + + def summary + self[:latest].summary + end + + def license + self[:latest].license + end + end + + def self.included(base_class) + base_class.extend(ClassMethods) + end + end + end +end diff --git a/lib/pgpm/package/sources.rb b/lib/pgpm/package/sources.rb new file mode 100644 index 0000000..bd2d006 --- /dev/null +++ b/lib/pgpm/package/sources.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Pgpm + class Package + module Sources + def sources + [] + end + end + end +end diff --git a/lib/pgpm/package/subscripting.rb b/lib/pgpm/package/subscripting.rb new file mode 100644 index 0000000..32a634e --- /dev/null +++ b/lib/pgpm/package/subscripting.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require "semver_dialects" + +module Pgpm + class Package + module Subscripting + module ClassMethods + def [](name) + if self == Pgpm::Package + all_subclasses.find { |klass| klass.package_name == name } + elsif name == :latest && package_versioning_scheme == :semver + return nil if package_versions.empty? + + version = package_versions.map { |ver| SemverDialects.parse_version("cargo", ver) }.last + new(version.to_s) + elsif name == :latest + null + elsif package_versions.include?(name) + new(name) + else + all_subclasses.find { |klass| klass.package_name == name } + end + end + end + + def self.included(base_class) + base_class.extend(ClassMethods) + end + end + end +end diff --git a/lib/pgpm/package/versioning.rb b/lib/pgpm/package/versioning.rb new file mode 100644 index 0000000..d86873b --- /dev/null +++ b/lib/pgpm/package/versioning.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Pgpm + class Package + module Versioning + module ClassMethods + def package_versions + warn("No versions defined for #{package_name}") + [] + end + + def package_versioning_scheme + @package_versioning_scheme ||= :semver + end + + def versioning_scheme(scheme) + @package_versioning_scheme = scheme + end + end + + def self.included(base_class) + base_class.extend(ClassMethods) + end + + attr_reader :version + end + end +end diff --git a/lib/pgpm/postgres.rb b/lib/pgpm/postgres.rb new file mode 100644 index 0000000..f9f2006 --- /dev/null +++ b/lib/pgpm/postgres.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require "semver_dialects" + +module Pgpm + module Postgres + class Distribution + def initialize(postgres_version) + @postgres_version = postgres_version + end + + def build_time_requirement_packages + raise NotImplementedError + end + end + + class RedhatBasedPGDG < Distribution + def build_time_requirement_packages + version = SemverDialects.parse_version("cargo", @postgres_version) + major = version.tokens.first + ["postgresql#{major}-#{version}-", "postgresql17-"] + end + end + end +end diff --git a/lib/pgpm/rpm.rb b/lib/pgpm/rpm.rb new file mode 100644 index 0000000..3fd93b7 --- /dev/null +++ b/lib/pgpm/rpm.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require "digest" +require "open-uri" + +module Pgpm + module RPM + end +end diff --git a/lib/pgpm/rpm/builder.rb b/lib/pgpm/rpm/builder.rb new file mode 100644 index 0000000..7769f8f --- /dev/null +++ b/lib/pgpm/rpm/builder.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Pgpm + module RPM + class Builder + def initialize(spec, os: nil) + @spec = spec + @os = os || Pgpm::OS.auto_detect + end + + def source_builder(target_directory = nil) + target_directory ||= "." + @os.with_scope do + dir = Dir.mktmpdir("pgpm") + File.open(Pathname(dir).join("#{@spec.package.name}.spec"), "w") do |specfile| + specfile.write(@spec.to_s) + specfile.close + sources = File.join(dir, "sources") + FileUtils.mkdir_p(sources) + @spec.package.sources.map do |src| + print "Downloading #{src.name}..." + srcfile = File.join(sources, src.name) + File.write(srcfile, src.read) + puts " done." + end + cfg = Pgpm::RPM::Mock::Config.new(@os.mock_config, path: target_directory) + Pgpm::RPM::Mock::Operation.buildsrpm(specfile.path, sources, config: cfg.path, result_dir: target_directory, cb: lambda { + FileUtils.rm_rf(dir) + }) + end + end + end + + def versionless_builder(target_directory = nil) + target_directory ||= "." + @os.with_scope do + dir = Dir.mktmpdir("pgpm") + File.open(Pathname(dir).join("#{@spec.package.name}.spec"), "w") do |specfile| + specfile.write(@spec.versionless) + specfile.close + cfg = Pgpm::RPM::Mock::Config.new(@os.mock_config, path: target_directory) + Pgpm::RPM::Mock::Operation.buildsrpm(specfile.path, nil, config: cfg.path, result_dir: target_directory, cb: lambda { + FileUtils.rm_rf(dir) + }) + end + end + end + + def self.builder(srpm, os: nil) + os ||= Pgpm::OS.auto_detect + os.with_scope do + target_directory ||= "." + cfg = Pgpm::RPM::Mock::Config.new(os.mock_config, path: target_directory) + srpm = [srpm] if srpm.is_a?(String) + srpm.reduce(nil) do |b, rpm| + op = Pgpm::RPM::Mock::Operation.rebuild(rpm, config: cfg.path, result_dir: target_directory) + b.nil? ? op : b.chain(op) + end + end + end + end + end +end diff --git a/lib/pgpm/rpm/mock/config.rb b/lib/pgpm/rpm/mock/config.rb new file mode 100644 index 0000000..dd9febe --- /dev/null +++ b/lib/pgpm/rpm/mock/config.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Pgpm + module RPM + module Mock + class Config + def initialize(source, path: nil) + @source = source + @path = path + @digest = Digest::SHA1.hexdigest(source) + end + + def path + f = Pathname(@path).join("mock-#{@digest}.cfg") + File.write(f, @source) if !File.exist?(f) || @digest != Digest::SHA1.hexdigest(File.read(f)) + f + end + end + end + end +end diff --git a/lib/pgpm/rpm/mock/operation.rb b/lib/pgpm/rpm/mock/operation.rb new file mode 100644 index 0000000..5d6bb22 --- /dev/null +++ b/lib/pgpm/rpm/mock/operation.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Pgpm + module RPM + module Mock + class Operation + def self.buildsrpm(spec, sources, config: nil, result_dir: nil, cb: nil) + buffer_result_dir = Dir.mktmpdir("pgpm") + args = ["--chain", "--buildsrpm", "--spec", spec, "--resultdir", + buffer_result_dir] + args.push("--sources", sources) if sources + args.push("-r", config.to_s) unless config.nil? + new(*args, cb: lambda { + rpms = Dir.glob("*.rpm", base: buffer_result_dir).map do |f| + FileUtils.cp(Pathname(buffer_result_dir).join(f), result_dir) unless result_dir.nil? + f + end + FileUtils.rm_rf(buffer_result_dir) + cb.call unless cb.nil? + rpms + }) + end + + def self.rebuild(srpm, config: nil, result_dir: nil, cb: nil) + buffer_result_dir = Dir.mktmpdir("pgpm") + args = ["--chain", "--rebuild", srpm, "--resultdir", buffer_result_dir] + args.push("-r", config.to_s) unless config.nil? + new(*args, cb: lambda { + rpms = Dir.glob("*.rpm", base: buffer_result_dir).map do |f| + FileUtils.cp(Pathname(buffer_result_dir).join(f), result_dir) unless result_dir.nil? + f + end + FileUtils.rm_rf(buffer_result_dir) + cb.call unless cb.nil? + rpms + }) + end + + def initialize(*args, cb: nil) + @args = args + @cb = cb + end + + attr_reader :args, :cb + + def call + command = "mock #{@args.join(" ")}" + raise "Failed to execute `#{command}`" unless system command + + @cb&.call + end + + def chain(op) + self.class.new(*(@args + op.args), cb: lambda { + res1 = @cb&.call + res2 = op.cb&.call + return res1 + res2 if res1.is_a?(Array) && res2.is_a?(Array) + + res2 + }) + end + + def and_then(op) + lambda do + res1 = call + res2 = op.call + return res1 + res2 if res1.is_a?(Array) && res2.is_a?(Array) + + [res1, res2] + end + end + end + end + end +end diff --git a/lib/pgpm/rpm/spec.rb b/lib/pgpm/rpm/spec.rb new file mode 100644 index 0000000..f243619 --- /dev/null +++ b/lib/pgpm/rpm/spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require "digest" +require "open-uri" + +module Pgpm + module RPM + class Spec + attr_reader :package, :release, :postgres_version, :postgres_distribution + + def initialize(package, postgres_version: nil, postgres_distribution: nil) + @package = package + @release = 1 + + @postgres_version = postgres_version || "17" + @postgres_distribution = postgres_distribution || "postgresql#{@postgres_version}" + end + + def versionless + <<~EOF + Name: pgpm-#{@package.name}-#{@postgres_version} + Version: #{@package.version} + Release: #{@release}%{?dist} + Summary: #{@package.summary} + License: #{@package.license} + + Requires: pgpm-#{@package.name}-#{@postgres_version}_#{@package.version} + BuildArch: noarch + + %description + #{@package.description} + + %build + + %clean + rm -rf $RPM_BUILD_ROOT + + %install + + %files + EOF + end + + def to_s + setup_opts = ["-q"] + if @package.source_url_directory_name + setup_opts.push("-n", @package.source_url_directory_name) + else + setup_opts.push("-n", "#{@package.name}-#{@package.version}") + end + sources = @package.sources + <<~EOF + Name: pgpm-#{@package.name}-#{@postgres_version}_#{@package.version} + Version: #{@package.version} + Release: 1%{?dist} + Summary: #{@package.summary} + License: #{@package.license} + #{sources.each_with_index.map { |src, index| "Source#{index}: #{src.name}" }.join("\n")} + + BuildRequires: #{@postgres_distribution} #{@postgres_distribution}-devel #{@postgres_distribution}-server + #{@package.build_dependencies.map { |dep| "BuildRequires: #{dep}" }.join("\n")} + Requires: #{@postgres_distribution} + + %description + #{@package.description} + + %prep + %setup #{setup_opts.join(" ")} + #{(sources[1..] || []).filter { |s| unpack?(s) }.each_with_index.map { |_src, index| "%setup -T -D #{setup_opts.join(" ")} -a #{index + 1}" }.join("\n")} + + export PG_CONFIG=$(rpm -ql #{@postgres_distribution} | grep 'pg_config$') + #{@package.configure_steps.map(&:to_s).join("\n")} + + %build + export PG_CONFIG=$(rpm -ql #{@postgres_distribution} | grep 'pg_config$') + export PGPM_BUILDROOT=%{buildroot} + #{@package.build_steps.map(&:to_s).join("\n")} + + %install + export PG_CONFIG=$(rpm -ql #{@postgres_distribution} | grep 'pg_config$') + export PGPM_BUILDROOT=%{buildroot} + find %{buildroot} -type f | sort - | sed 's|^%{buildroot}||' > .pgpm_before | sort + #{@package.install_steps.map(&:to_s).join("\n")} + find %{buildroot} -type f | sort - | sed 's|^%{buildroot}||' > .pgpm_after | sort + comm -13 .pgpm_before .pgpm_after | sort -u > filelist.txt + + %files -f filelist.txt + + + %changelog + EOF + end + + private + + def unpack?(src) + src = src.name if src.respond_to?(:name) + src.to_s.end_with?(".tar.gz") + end + end + end +end diff --git a/lib/pgpm/version.rb b/lib/pgpm/version.rb new file mode 100644 index 0000000..48768ba --- /dev/null +++ b/lib/pgpm/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Pgpm + VERSION = "0.1.0" +end diff --git a/packages/omnigres/extension_discovery.rb b/packages/omnigres/extension_discovery.rb new file mode 100644 index 0000000..711b3ea --- /dev/null +++ b/packages/omnigres/extension_discovery.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require "git" +require "parallel" + +module Omnigres + class ExtensionDiscovery + @@extensions = {} + @@git_revisions = {} + + def initialize(revision: nil, path: nil) + return if @@extensions[revision] + + suffix = revision ? "-#{revision}" : nil + path ||= Pgpm::Cache.directory.join("omnigres#{suffix}") + git = + if File.directory?(path) + ::Git.open(path) + else + ::Git.clone("https://github.com/omnigres/omnigres", path) + end + git.checkout(revision) if revision + @git = git + end + + attr_reader :git + + def extension_versions + process_git_log + @@extensions[@git.log.first.sha] + end + + def extension_git_revisions + process_git_log + @@git_revisions[@git.log.first.sha] + end + + private + + def process_git_log + return if @@extensions[@git.log.first.sha] + + merges = @git.log(:all).select { |c| c.parents.size > 1 } + commit_merges = Hash[Parallel.flat_map(merges) do |m| + before = m.parents[0].sha + after = m.parents[1].sha + commits = git.log.between(before, after).map(&:sha) + commits.product([m.sha]) + end] + + versions_maps = @git.log(:all).path("versions.txt").flat_map do |c| + Hash[@git.show(c, + "versions.txt").split("\n").map do |l| + ext, ver = l.split("=") + [[ext, c.sha], ver] + end] + end + extensions = versions_maps.flat_map(&:keys).map(&:first).uniq + @@extensions[@git.log.first.sha] = Hash[extensions.map do |e| + [e, versions_maps.map do |m| + m.transform_keys(&:first)[e] + end.compact.uniq.map { |v| SemverDialects.parse_version("cargo", v) }.sort.map(&:to_s)] + end] + last_hashes = Hash[versions_maps.flat_map do |h| + h.each_pair.map { |(ext, sha), ver| [[ext, ver], sha] } + end] + @@git_revisions[@git.log.first.sha] = last_hashes.each_with_object({}) do |((name, version), sha), result| + result[name] ||= {} + result[name][version] = commit_merges[sha] + end + end + end +end diff --git a/packages/omnigres/omni.rb b/packages/omnigres/omni.rb new file mode 100644 index 0000000..9dec2b2 --- /dev/null +++ b/packages/omnigres/omni.rb @@ -0,0 +1,174 @@ +# frozen_string_literal: true + +require "minitar" +require "find" +require "zlib" +require "progress" + +module Omnigres + class Omni < Pgpm::Package + github "omnigres/omnigres", download_version_tags: false + + def initialize(version) + @fetch_previous_version = true + super + end + + def summary + "Advanced adapter for Postgres extensions" + end + + def description + summary + end + + def self.package_versions + ExtensionDiscovery.new.extension_versions["omni"] + end + + def version_git_commit + ExtensionDiscovery.new.extension_git_revisions["omni"][version] + end + + def configure_steps + ["export PIP_CONFIG_FILE=$(pwd)/deps/pip.conf", + "cmake -S extensions/omni -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo " \ + "-DOPENSSL_CONFIGURED=1 -DPG_CONFIG=$PG_CONFIG -DCPM_SOURCE_CACHE=$(pwd)/deps/_deps"] + end + + def build_steps + [ + "cmake --build build --parallel --target package_omni_extension", + "cmake --build build --parallel --target package_omni_migrations", + "scripts/generate_upgrade.sh #{name} #{previous_version.version} " \ + "$(pwd)/omnigres-#{previous_version.version_git_commit} #{version} $(pwd)" + ] + end + + def install_steps + [ + "mkdir -p $PGPM_BUILDROOT/$($PG_CONFIG --sharedir)/extension", + "mkdir -p $PGPM_BUILDROOT/$($PG_CONFIG --pkglibdir)", + "cp build/packaged/*.so $PGPM_BUILDROOT/$($PG_CONFIG --pkglibdir)", + "cp build/packaged/extension/*.control $PGPM_BUILDROOT/$($PG_CONFIG --sharedir)/extension", + "cp build/packaged/extension/*.sql $PGPM_BUILDROOT/$($PG_CONFIG --sharedir)/extension" + ] + end + + def license + "Apache 2.0" + end + + def build_dependencies + %w[cmake openssl-devel python3 python3-devel nc sudo] + super + end + + def dependencies + ["openssl"] + super + end + + def source_url_directory_name + "omnigres-#{version_git_commit}" + end + + class UnsupportedVersion < StandardError + def message + "This version of omnigres extension is too old" + end + end + + def deps_tar_gz(dir_name = "deps") + @os = Pgpm::OS.auto_detect + if @os.is_a?(Pgpm::OS::RedHat) + system "sudo dnf -y install libxml2-devel libxslt-devel cmake gcc g++ python3-devel" + end + recipe_hash = Digest::SHA1.hexdigest(File.read(__FILE__) + scripts_tar_gz.read) + deps = File.join(source, dir_name) + src = source.to_s + ready_marker = File.join(src, ".complete.#{recipe_hash}") + targz = File.join(src, "deps-#{recipe_hash}.tar.gz") + unless File.exist?(ready_marker) + raise UnsupportedVersion unless File.directory?(File.join(src, "cmake", "dependencies")) + # FIXME: this is not perfect as we're still required to have the build-time dependencies + # required for this (such as g++, python3-devel, cmake, g++, etc...) + unless system "cmake -S #{src}/cmake/dependencies -B #{deps} -DCPM_SOURCE_CACHE=#{deps}/_deps" + raise "Can't fetch dependencies" + end + + begin + sgz = Zlib::GzipWriter.new(File.open(targz, "wb")) + tar = Minitar::Output.open(sgz) + files = Find.find(deps).filter do |entry| + # Don't include .git + Pathname(entry).each_filename.map { |f| f == ".git" }.none? + end + files.with_progress("Preparing #{dir_name}.tar.gz") do |entry| + stat = File.stat(entry) + data = File.directory?(entry) ? nil : File.binread(entry) + info = { name: Pathname(entry).relative_path_from(src).to_s, + mode: stat.mode, uid: stat.uid, gid: stat.gid, mtime: stat.mtime } + Minitar.pack_as_file(info, data, tar) + end + ensure + tar.close + end + FileUtils.touch(ready_marker) + end + Pgpm::OnDemandFile.new("#{dir_name}.tar.gz", -> { File.open(targz) }) + end + + attr_accessor :fetch_previous_version + + def sources + srcs = super + srcs.push(deps_tar_gz) + if fetch_previous_version && previous_version + begin + prev_version = previous_version + puts "Fetching previous version #{prev_version.version} to be able to generate migrations" + prev_version.fetch_previous_version = false + srcs.push(prev_version.sources[0]) # archive + srcs.push(prev_version.deps_tar_gz("deps-prev")) # deps + rescue UnsupportedVersion + # ignore this one, just don't build an upgrade + puts "Ignore #{prev_version.version}, it is unsupported" + end + end + + srcs.push(scripts_tar_gz) + srcs + end + + private + + def previous_version + return @previous_version if @previous_version + + sorted_versions = self.class.package_versions.sort_by { |v| SemverDialects.parse_version("cargo", v) }.map(&:to_s) + index = sorted_versions.index(version) + return unless index.positive? + + @previous_version = self.class[sorted_versions[index - 1]] + end + + def scripts_tar_gz + s = String.new + begin + dir = File.join(File.dirname(__FILE__), "scripts") + sgz = Zlib::GzipWriter.new(StringIO.new(s)) + tar = Minitar::Output.open(sgz) + Find.find(dir) do |entry| + stat = File.stat(entry) + data = File.directory?(entry) ? nil : File.binread(entry) + info = { name: Pathname(entry).relative_path_from(File.dirname(__FILE__)).to_s, + mode: stat.mode, uid: stat.uid, gid: stat.gid, mtime: stat.mtime } + Minitar.pack_as_file(info, data, tar) + end + ensure + # Closes both tar and sgz. + tar.close + end + Pgpm::OnDemandFile.new("scripts.tar.gz", -> { StringIO.open(s) }) + end + end +end diff --git a/packages/omnigres/scripts/generate_upgrade.sh b/packages/omnigres/scripts/generate_upgrade.sh new file mode 100755 index 0000000..54dd439 --- /dev/null +++ b/packages/omnigres/scripts/generate_upgrade.sh @@ -0,0 +1,138 @@ +#! /usr/bin/env bash + +script_dir=$(dirname "$0") + +BUILD_TYPE=${BUILD_TYPE:=RelWithDebInfo} +DEST_DIR=${DESTDIR:=$script_dir} + +PG_CONFIG=${PG_CONFIG:=pg_config} +PG_BINDIR=$($PG_CONFIG --bindir) +PG_LIBDIR=$($PG_CONFIG --pkglibdir) +PGSHAREDIR=$($PG_CONFIG --sharedir) + +ext_name=$1 +old_ver=$2 +old_ver_path=$3 +new_ver=$4 +new_ver_path=$5 + +# for now, just for omni +new_ver_omni_ver=$(cat ${new_ver_path}/versions.txt | grep "^$ext_name=" | cut -d "=" -f 2) +old_ver_omni_ver=$(cat ${old_ver_path}/versions.txt | grep "^$ext_name=" | cut -d "=" -f 2) + +set -xe + +# Build the old extension +PIP_CONFIG_FILE=${new_ver_path}/deps-prev/pip.conf cmake -S "${old_ver_path}/extensions/${ext_name}" -B "${old_ver_path}/build" -DCPM_SOURCE_CACHE=$new_ver_path/deps-prev/_deps -DPG_CONFIG=$PG_CONFIG -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DOPENSSL_CONFIGURED=1 +cmake --build "${old_ver_path}/build" --parallel --target package_omni_extension +cmake --build "${old_ver_path}/build" --parallel --target package_omni_migrations +cmake --build "${old_ver_path}/build" --parallel --target package_${ext_name}_extension +cmake --build "${old_ver_path}/build" --parallel --target package_${ext_name}_migrations +# + +file $old_ver_path/build/packaged/omni--$old_ver_omni_ver.so +file $new_ver_path/build/packaged/omni--$new_ver_omni_ver.so + +# Determine the path to the extension in the new version +extpath=$(cat ${new_ver_path}/build/paths.txt | grep "^$ext_name " | cut -d " " -f 2) + +# List migrations in new version +new_migrations=() +new_migrate_path="$new_ver_path/$extpath/migrate" +if [ -d "$new_migrate_path/$ext_name" ]; then + # Extension that shares a folder with another extension + new_migrate_path="$new_migrate_path/$ext_name" +fi +for file in "$new_migrate_path/"*.sql; do + new_migrations+=($(basename "$file")) +done + +echo $new_migrations + +# Determine the path to the extension in the old version +extpath=$(cat ${old_ver_path}/build/paths.txt | grep "^$ext_name " | cut -d " " -f 2) +# List migrations in old version +old_migrations=() +old_migrate_path="$old_ver_path/$extpath/migrate" +if [ -d "$old_migrate_path/$ext_name" ]; then + # Extension that shares a folder with another extension + old_migrate_path="$old_migrate_path/$ext_name" +fi +for file in "$old_migrate_path/"*.sql; do + old_migrations+=($(basename "$file")) +done + +echo $old_migrations + + +for mig in ${new_migrations[@]}; do + if [ ! -f "$old_migrate_path/$mig" ]; then + $new_ver_path/build/inja/inja "$new_migrate_path/$mig" >> "$DEST_DIR/$ext_name--$old_ver--$new_ver.sql" + # Ensure a new line + echo >> "$DEST_DIR/$ext_name--$old_ver--$new_ver.sql" + fi +done + +cat "$DEST_DIR/$ext_name--$old_ver--$new_ver.sql" + +# Now, we need to replace functions that have changed +# For this, we'll: +# * prepare a database +db="$DEST_DIR/db_$ext_name-$old_ver-$new_ver" +rm -rf "$db" +chown -R postgres "$DEST_DIR" +sudo -u postgres "$PG_BINDIR/initdb" -D "$db" --no-clean --no-sync --locale=C --encoding=UTF8 +sockdir=$(sudo -u postgres mktemp -d) +# * install the extension in this revision, snapshot pg_proc, drop the extension +# We copy all scripts because there are dependencies +cp -v "$old_ver_path"/build/packaged/extension/*.sql "$old_ver_path"/build/packaged/extension/*.control "$PGSHAREDIR/extension" +cp -v "$old_ver_path"/build/packaged/*.so "$PG_LIBDIR" +sudo -u postgres "$PG_BINDIR/pg_ctl" start -D "$db" -o "-c max_worker_processes=64" -o "-c listen_addresses=''" -o "-k $sockdir" -o "-c shared_preload_libraries='$PG_LIBDIR/omni--$old_ver_omni_ver.so'" +sudo -u postgres "$PG_BINDIR/createdb" -h "$sockdir" "$ext_name" +cat <> "$DEST_DIR/$ext_name--$old_ver--$new_ver.sql" + -- Changed code + select pg_get_functiondef(pg_proc.oid) || ';' from (select * from pg_proc where oid not in (select aggfnoid from pg_aggregate)) as pg_proc + inner join pg_language on pg_language.oid = pg_proc.prolang and pg_language.lanname not in ('c', 'internal') + inner join pg_namespace on pg_namespace.oid = pg_proc.pronamespace and pg_namespace.nspname = '$ext_name' + inner join procs on + ((procs.proname::text = pg_proc.proname::text) and procs.pronamespace = pg_proc.pronamespace and + pg_get_function_identity_arguments(pg_proc.oid) = identity_args and pg_get_functiondef(pg_proc.oid) != procs.src); +EOF +# test the upgrade +# first, let's stop current database to load older extension again +sudo -u postgres "$PG_BINDIR/pg_ctl" stop -D "$db" -m smart +cp "$DEST_DIR/$ext_name--$old_ver--$new_ver.sql" "$old_ver_path/build/packaged/extension/$ext_name--$old_ver.control" "$old_ver_path/build/packaged/extension/$ext_name--$old_ver.sql" "$PGSHAREDIR/extension" +sudo -u postgres "$PG_BINDIR/pg_ctl" start -D "$db" -o "-c max_worker_processes=64" -o "-c listen_addresses=''" -o "-k $sockdir" -o "-c shared_preload_libraries='$PG_LIBDIR/omni--$old_ver_omni_ver.so'" +cat < 1.0" + + spec.add_dependency "dry-cli", "~> 1.1.0" + spec.add_dependency "dry-inflector", "~> 1.1.0" + spec.add_dependency "git", "~> 2.3.0" + spec.add_dependency "lspace", "~> 0.14" + spec.add_dependency "minitar", "~> 1.0.2" + spec.add_dependency "oj", "~> 3.16.6" + spec.add_dependency "parallel", "~> 1.26.3" + spec.add_dependency "progress", "~> 3.6.0" + spec.add_dependency "semver_dialects", "~> 3.4.3" + spec.add_dependency "xdg", "~> 8.7.0" + spec.add_dependency "zeitwerk", "~> 2.6.18" + spec.add_dependency "zlib", "~> 3.1.1" + + # For more information and examples about making a new gem, check out our + # guide at: https://bundler.io/guides/creating_gem.html +end diff --git a/sig/pgpm.rbs b/sig/pgpm.rbs new file mode 100644 index 0000000..8f93435 --- /dev/null +++ b/sig/pgpm.rbs @@ -0,0 +1,4 @@ +module Pgpm + VERSION: String + # See the writing guide of rbs: https://github.com/ruby/rbs#guides +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..7e5d850 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require "pgpm" + +require "minitest/autorun" diff --git a/test/test_pgpm.rb b/test/test_pgpm.rb new file mode 100644 index 0000000..933450b --- /dev/null +++ b/test/test_pgpm.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require "test_helper" + +class TestPgpm < Minitest::Test + def test_that_it_has_a_version_number + refute_nil ::Pgpm::VERSION + end +end