Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@
[submodule "examples/c-qrcode/QR-Code-generator"]
path = examples/c-qrcode/QR-Code-generator
url = https://github.com/nayuki/QR-Code-generator.git
[submodule "examples/c-rpm/rpm"]
path = examples/c-rpm/rpm
url = https://github.com/rpm-software-management/rpm.git
96 changes: 96 additions & 0 deletions examples/c-rpm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# RPM Haskell Bindings

This example demonstrates generating Haskell bindings for
[RPM Package Manager](https://github.com/rpm-software-management/rpm), a
powerful package management system.

## Prerequisites

### Using Nix Shell

This example includes a `shell.nix` file that provides all necessary
dependencies for building RPM from source. The shell environment includes:

**Build tools:**
- `cmake` - Build system generator
- `pkg-config` - Helper tool for compile/link flags
- `scdoc` - Documentation generator

**RPM dependencies:**
- `popt` - Command line option parsing
- `libarchive` - Multi-format archive and compression library
- `sqlite` - Database engine for RPM database
- `zstd` - Fast compression algorithm
- `elfutils` - ELF object file access library
- `xz` - LZMA compression utilities
- `lua` - Scripting language for RPM
- `zlib` - Compression library
- `bzip2` - Compression library
- `readline` - Command line editing
- `file` - File type identification
- `libcap` - POSIX capabilities library
- `audit` - Linux audit framework
- `libselinux` - SELinux runtime library
- `dbus` - Message bus system
- `rpm-sequoia` - OpenPGP implementation (from nixos-unstable)

> [!TIP]
>
> If you are using Nix or NixOS, please also have a look at our [`hs-bindgen`
> Nix tutorial](https://github.com/well-typed/hs-bindgen-tutorial-nix).

If not using Nix at all, take a look at the [INSTALL](./rpm/INSTALL) file for more details
on the instructions of how to build `rpm`.

## Running the Example

### Building RPM from Source

According to the instructions in [INSTALL](./rpm/INSTALL):

```sh
# On the `rpm` folder
mkdir _build
cd _build
cmake ..
make
make install
```

Once all these steps have completed successfully the libraries will be under
`rpm/_build/_install/lib64`.

### Linking Against the C Library

The `.cabal` file includes `extra-libraries: rpm, rpmio`, which tells the
linker to link against both `librpm.so` and `librpmio.so`. RPM is split into
multiple libraries, and both are needed for the bindings to work correctly.

### Generating the bindings

```bash
./generate-and-run.sh
```

### Generating the Include Graph

This script will already generate the bindings by their right order, i.e. the
header include dependency order. If for any reason the `rpm` library updated,
then it might be necessary to change the `generate-and-run` script so that it
follows the right order.


To visualize the header dependencies, you can generate an include graph using
`hs-bindgen-cli`:

```bash
cd examples/c-rpm
cabal run hs-bindgen-cli -- info include-graph \
-I rpm/include \
-o output-graph.md \
rpm/rpmlib.h
```

This generates a Mermaid diagram showing all header includes and their
dependencies. You can visualize it at [mermaid.live](https://mermaid.live/) or
in any Markdown viewer that supports Mermaid diagrams (like GitHub).
149 changes: 149 additions & 0 deletions examples/c-rpm/generate-and-run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#!/usr/bin/env bash

# Exit on first error
set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
INCLUDE_DIR="$SCRIPT_DIR/rpm/include"
LIB_DIR="$SCRIPT_DIR/rpm/_build/_install/lib64"
BINDING_SPEC_DIR="$SCRIPT_DIR/binding-specs"
HS_OUTPUT_DIR="$SCRIPT_DIR/hs-project/src/"

echo $SCRIPT_DIR

# Create directories
mkdir -p "$BINDING_SPEC_DIR"
mkdir -p "$HS_OUTPUT_DIR"

echo "# "
echo "# Generating Haskell bindings in dependency order"
echo "# "

cd "$PROJECT_ROOT"

# Helper function to generate bindings with optional external binding specs
# Usage: generate_bindings HEADER MODULE_NAME [--no-binding-spec] [external specs...]
generate_bindings() {
local HEADER="$1"
local MODULE_NAME="$2"
shift 2

local BINDING_SPEC_FILE="$BINDING_SPEC_DIR/${HEADER%.h}.yaml"
local EXTERNAL_SPECS=("$@")

echo "Generating bindings for $HEADER -> $MODULE_NAME"

local CMD=(
cabal run hs-bindgen-cli -- preprocess
-I "$INCLUDE_DIR"
--hs-output-dir "$HS_OUTPUT_DIR"
--create-output-dirs
--module "$MODULE_NAME"
--parse-all
--select-from-main-headers
--enable-program-slicing
--gen-binding-spec "$BINDING_SPEC_FILE"
)

# Add external binding specs if any
for spec in "${EXTERNAL_SPECS[@]}"; do
if [ -f "$spec" ]; then
CMD+=(--external-binding-spec "$spec")
fi
done

CMD+=("rpm/$HEADER")

"${CMD[@]}"
}

# Generate bindings in dependency order

# 1. rpmtypes.h (no dependencies)
generate_bindings "rpmtypes.h" "RPM.Types"

# 2. rpmsw.h (no dependencies)
generate_bindings "rpmsw.h" "RPM.Sw"

# 3. rpmutil.h (no dependencies)
generate_bindings "rpmutil.h" "RPM.Util"

# 4. rpmtag.h (depends on rpmtypes)
generate_bindings "rpmtag.h" "RPM.Tag" \
"$BINDING_SPEC_DIR/rpmtypes.yaml"

# 5. argv.h (depends on rpmtypes)
generate_bindings "argv.h" "RPM.Argv" \
"$BINDING_SPEC_DIR/rpmtypes.yaml"

# 6. rpmprob.h (depends on rpmtypes)
generate_bindings "rpmprob.h" "RPM.Prob" \
"$BINDING_SPEC_DIR/rpmtypes.yaml"

# 7. rpmio.h (depends on rpmtypes, rpmsw)
generate_bindings "rpmio.h" "RPM.IO" \
"$BINDING_SPEC_DIR/rpmtypes.yaml" \
"$BINDING_SPEC_DIR/rpmsw.yaml"

# 8. rpmtd.h (depends on rpmtag, argv)
generate_bindings "rpmtd.h" "RPM.Td" \
"$BINDING_SPEC_DIR/rpmtag.yaml" \
"$BINDING_SPEC_DIR/argv.yaml"

# 9. rpmps.h (depends on rpmtypes, rpmprob)
generate_bindings "rpmps.h" "RPM.Ps" \
"$BINDING_SPEC_DIR/rpmtypes.yaml" \
"$BINDING_SPEC_DIR/rpmprob.yaml"

# 10. header.h (depends on rpmio, rpmtypes, rpmtd, rpmutil)
generate_bindings "header.h" "RPM.Header" \
"$BINDING_SPEC_DIR/rpmio.yaml" \
"$BINDING_SPEC_DIR/rpmtypes.yaml" \
"$BINDING_SPEC_DIR/rpmutil.yaml" \
"$BINDING_SPEC_DIR/rpmtd.yaml"

# 11. rpmds.h (depends on rpmtypes, rpmutil, rpmps)
generate_bindings "rpmds.h" "RPM.Ds" \
"$BINDING_SPEC_DIR/rpmtypes.yaml" \
"$BINDING_SPEC_DIR/rpmutil.yaml" \
"$BINDING_SPEC_DIR/rpmps.yaml"

# 12. rpmver.h (depends on rpmtypes, rpmds)
generate_bindings "rpmver.h" "RPM.Ver" \
"$BINDING_SPEC_DIR/rpmtypes.yaml" \
"$BINDING_SPEC_DIR/rpmds.yaml"

# 13. rpmlib.h (depends on rpmio, header, rpmtag, rpmds, rpmver)
generate_bindings "rpmlib.h" "RPM.Lib" \
"$BINDING_SPEC_DIR/rpmio.yaml" \
"$BINDING_SPEC_DIR/header.yaml" \
"$BINDING_SPEC_DIR/rpmtag.yaml" \
"$BINDING_SPEC_DIR/rpmds.yaml" \
"$BINDING_SPEC_DIR/rpmver.yaml" \

echo "# "
echo "# Creating cabal.project.local"
echo "# "

cat > "$SCRIPT_DIR/hs-project/cabal.project.local" <<EOF
package c-rpm
extra-include-dirs:
$INCLUDE_DIR
extra-lib-dirs:
$LIB_DIR
EOF

cat "$SCRIPT_DIR/hs-project/cabal.project.local"

echo "# "
echo "# Done!"
echo "# "
echo "Running the project"

cd "$SCRIPT_DIR/hs-project"
LD_LIBRARY_PATH="$RPM_LIB_DIR:$LD_LIBRARY_PATH"
export LD_LIBRARY_PATH

cabal build
cabal run c-rpm
5 changes: 5 additions & 0 deletions examples/c-rpm/hs-project/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Revision history for c-qrcode

## 0.1.0.0 -- YYYY-mm-dd

* First version. Released on an unsuspecting world.
29 changes: 29 additions & 0 deletions examples/c-rpm/hs-project/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Copyright (c) 2024-2025, Well-Typed LLP and Anduril Industries Inc.


Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.

* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
76 changes: 76 additions & 0 deletions examples/c-rpm/hs-project/app/Main.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
module Main where

import Foreign.C.String (withCString, peekCString, castCCharToChar)
import Foreign.Marshal.Array (peekArray)
import Foreign.Ptr (nullPtr)
import Foreign.Storable (peek)
import Foreign (with)

import RPM.Argv.Safe qualified as RPM
import RPM.Argv (ARGV_t(..), ARGV_const_t(..))

main :: IO ()
main = do
putStrLn "RPM String Utilities Demo"
putStrLn "=========================="
putStrLn ""

-- Example 1: Split a string into words
putStrLn "Example 1: Splitting a path string"
putStrLn "-----------------------------------"
let pathString = "/usr/bin:/usr/local/bin:/opt/bin"
putStrLn $ "Input: \"" ++ pathString ++ "\""

argv1 <- RPM.argvNew
with argv1 $ \argvPtr -> do
_ <- withCString pathString $ \strPtr ->
withCString ":" $ \sepPtr ->
RPM.argvSplit argvPtr strPtr sepPtr

argv1' <- peek argvPtr
count1 <- RPM.argvCount (ARGV_const_t (un_ARGV_t argv1'))
putStrLn $ "Split into " ++ show count1 ++ " parts:"

arrayPtr1 <- peek (un_ARGV_t argv1')
when (arrayPtr1 /= nullPtr) $ do
cstrs <- peekArray (fromIntegral count1) arrayPtr1
let strings = map castCCharToChar cstrs
mapM_ (\(i, s) -> putStrLn $ " [" ++ show (i :: Int) ++ "] " ++ s) (zip [0..] [strings])

_ <- RPM.argvFree argv1'
pure ()

putStrLn ""

-- Example 2: Split and rejoin with different separator
putStrLn "Example 2: Transform separator"
putStrLn "-------------------------------"
let csvString = "apple,banana,cherry,date"
putStrLn $ "Input: \"" ++ csvString ++ "\""
putStrLn "Action: Split by ',' and join with ' | '"

argv2 <- RPM.argvNew
with argv2 $ \argvPtr -> do
-- Split by comma
_ <- withCString csvString $ \strPtr ->
withCString "," $ \sepPtr ->
RPM.argvSplit argvPtr strPtr sepPtr

argv2' <- peek argvPtr

-- Join with pipe
withCString " | " $ \joinSep -> do
resultPtr <- RPM.argvJoin (ARGV_const_t (un_ARGV_t argv2')) joinSep
when (resultPtr /= nullPtr) $ do
result <- peekCString resultPtr
putStrLn $ "Output: \"" ++ result ++ "\""

_ <- RPM.argvFree argv2'
pure ()

putStrLn ""
putStrLn "✓ Success!"

when :: Bool -> IO () -> IO ()
when True action = action
when False _ = pure ()
Loading