Compare commits

...

21 Commits

Author SHA1 Message Date
7460d784b6 add search and back button to docs 2026-01-23 18:06:07 +11:00
c9976b9c08 bump version number 2026-01-23 17:22:53 +11:00
9075342661 removed uneeded print and removed packages from project 2026-01-23 17:21:59 +11:00
904ef85f54 package wip 2026-01-23 16:47:56 +11:00
f6bfa6b8fd Merge branch 'main' of https://chookspace.com/ground/Digpkg 2026-01-23 16:39:42 +11:00
5bc61e30a6 fixed numerous bugs 2026-01-23 16:38:53 +11:00
da59155954 turned digpkg into a pypi package 2026-01-22 21:35:38 +11:00
e75da2f297 removed uneeded printing 2026-01-22 09:27:57 +11:00
6823f64541 "Raises" section in autogenerated markdown, warning for uploading a package ending in _build 2026-01-22 06:08:36 +11:00
d8cddfbbd4 dig docs command 2026-01-21 20:53:24 +11:00
b4fd43fb9b removed uneeded file 2026-01-21 08:38:33 +11:00
941cf8f1f3 removed sqlite3, too complicated and not needed for now 2026-01-20 19:41:34 +11:00
bb3c2f3225 address boundary error not my beloved 2026-01-20 18:52:48 +11:00
0f95dff95f include folder name in build note 2026-01-20 18:28:44 +11:00
93ebd3d8b9 sh bang 2026-01-20 18:05:58 +11:00
90192be2b1 working on bindings for sqlite3 2026-01-20 17:13:04 +11:00
9634750185 list command fix 2026-01-20 07:58:30 +11:00
b60369bf4a extra note in build command 2026-01-20 07:53:52 +11:00
a12990ef65 added the build command 2026-01-20 07:47:15 +11:00
77a7a44804 install dependencies 2026-01-19 21:06:25 +11:00
2f1412a492 force specification of version number
this is cause gitea doesn't allow you to just grab the latest version of packages
2026-01-19 19:16:59 +11:00
14 changed files with 345 additions and 29 deletions

1
dig/__init__.py Normal file
View File

@@ -0,0 +1 @@
from .main import main

133
dig/build.py Normal file
View File

@@ -0,0 +1,133 @@
from .util import *
import shutil
import os, sys
import subprocess
import configparser
from rich.console import Console
console = Console()
def find_c_files(path: str):
paths = []
for entry in os.listdir(path):
full_path = os.path.join(path, entry)
if os.path.isdir(full_path):
find_c_files(full_path)
else:
if full_path.endswith(".c"):
paths.append(full_path)
return paths
def build_mineral(args):
# sanity checks
if not os.path.isdir(args.folder_path):
console.print("[b red]digpkg: failed to build mineral: specified folder doesn't exist![/]")
sys.exit(1)
if os.path.isfile(os.path.join(args.folder_path, "main.so")):
console.print("[b red]digpkg: failed to build mineral: attempt to build compiled mineral![/]")
sys.exit(1)
if not shutil.which("gcc"):
console.print("[b red]digpkg: failed to build mineral: gcc was not found![/]")
sys.exit(1)
# use gcc to compile the mineral
with console.status("Compiling", spinner="bouncingBall", spinner_style="green") as status:
c_files = find_c_files(args.folder_path)
if len(c_files) == 0:
console.print("[b red]digpkg: failed to build mineral: no .c files found in the specified folder, are you sure you're compiling a mineral?[/]")
sys.exit(1)
if subprocess.run([
"gcc",
"-shared",
"-fPIC",
*c_files,
*[f"-{arg}" for arg in (args.gcc_args or [])],
"-o",
"main.so"
], stdout=None).returncode:
console.print("[b red]digpkg: failed to build mineral: gcc exited with non-zero exit code.[/]")
sys.exit(1)
console.print("[:white_check_mark:] Compile success!")
def build(args):
if args.package:
# build the mineral
build_mineral(args)
# create the build dir and throw in the .so file
with console.status("Packaging...", spinner="bouncingBall", spinner_style="green"):
build_dir = f"{os.path.basename(args.folder_path)}_build"
if not os.path.isdir(build_dir):
os.mkdir(build_dir)
# generate a mineral.ini file
config_parser = configparser.ConfigParser()
config_parser["package"] = {
"description": "Your description here",
"version": "1.0.0",
"config_version": "1",
}
config_parser["dependencies"] = {}
# write it to our new mineral
with open(os.path.join(build_dir, "mineral.ini"), "w") as f:
config_parser.write(f)
# generate doc files
docs_folder = os.path.join(build_dir, "docs")
if not os.path.isdir(docs_folder):
os.mkdir(docs_folder)
with open(os.path.join(docs_folder, "my_function.md"), "w") as f:
f.write("# mylib_MyFunction\n")
f.write("Describe your function briefly.\n\n")
f.write("## Arguments\n")
f.write("- myArgument (double): describe your arguments in a list format like this.\n\n")
f.write("## Returns\n")
f.write("result (int): then explain what the function returns\n\n")
f.write("## Raises\n")
f.write("- `ErrorName`: have your error reason here, include this if your function raises any errors\n")
f.write("## Example\n")
f.write("```python\n")
f.write("# then show how you use the function in an example\n")
f.write("call !mylib_MyFunction 123.0 &result\n")
f.write("```\n")
f.write("`Using \"python\" as the syntax highlighting language seems to work well with Ground.`")
with open(os.path.join(build_dir, "SUMMARY.md"), "w") as f:
f.write("# mylib\n")
f.write("Introduce your module here!\n\nThis file will serve as an index page for all your docs.\n\n")
f.write("## Subtitle (use this for categories)\n")
f.write("- [mylib_MyFunction](docs/my_function.md)\n\n")
f.write("## Changelog\n")
f.write("### v0.0.1 (latest)\n")
f.write("- Initial release.\n")
console.print("[:white_check_mark:] Generated a new package for you!")
shutil.move("main.so", os.path.join(build_dir, "main.so"))
console.print("[:white_check_mark:] Put your library into your package!")
else:
check_sudo()
check_ground_libs_path()
# build the mineral and move it straight to the ground libs folder
build_mineral(args)
with console.status("Installing...", spinner="bouncingBall", spinner_style="green"):
shutil.move("main.so", os.path.join(os.getenv("GROUND_LIBS"), f"{os.path.basename(args.folder_path)}.so"))
console.print("[:white_check_mark:] Installed!")

112
dig/docs.py Normal file
View File

@@ -0,0 +1,112 @@
from .util import *
import os, sys
from rich.console import Console
from textual.app import App, ComposeResult
from textual.widgets import MarkdownViewer, Header, Footer, Input, Button
from textual.containers import Horizontal
from textual_autocomplete import PathAutoComplete
from textual import on
console = Console()
class DocsApp(App):
TITLE = "Digpkg Docs"
SUB_TITLE = "made with ❤️ by SpookyDervish"
CSS = """
#search {
margin: 1;
width: 1fr;
}
#back {
margin-top: 1;
margin-left: 1;
}
#bottom {
height: 5;
}
"""
def __init__(self, inital_markdown_path: str):
super().__init__()
self.inital_markdown_path = inital_markdown_path
self.docs_folder = os.path.join(os.path.dirname(self.inital_markdown_path), "docs")
async def on_input_submitted(self, event: Input.Submitted):
if event.input.id == "search":
if not os.path.isdir(self.docs_folder):
return
file_path = os.path.join(self.docs_folder, event.input.value)
if os.path.isfile(file_path):
event.input.clear()
event.input.focus()
await self.query_one(MarkdownViewer).go(file_path)
else:
self.notify("That file wasn't found.", title="Uh oh", severity="error")
async def on_button_pressed(self, event: Button.Pressed):
if event.button.id == "back":
await self.query_one(MarkdownViewer).back()
def compose(self) -> ComposeResult:
with open(self.inital_markdown_path, "r") as f:
markdown = f.read()
yield Header()
yield MarkdownViewer(markdown)
with Horizontal(id="bottom"):
yield Button(label="Back", id="back", flat=True, variant="error")
search_input = Input(id="search", placeholder="Search . . .")
yield search_input
if os.path.isdir(self.docs_folder):
yield PathAutoComplete(
search_input,
path=self.docs_folder
)
else:
search_input.disabled = True
search_input.tooltip = "This mineral doesn't have a docs folder and can't be searched."
yield Footer()
def docs(args):
check_ground_libs_path()
mineral_path = os.path.join(os.getenv("GROUND_LIBS"), args.mineral_name)
if not os.path.isdir(mineral_path):
console.print(f"[b red]digpkg: can't read docs: the mineral [i]{args.mineral_name}[/] was not found!")
sys.exit(1)
docs_path = os.path.join(mineral_path, "docs")
summary_md_file = os.path.join(mineral_path, "SUMMARY.md")
if args.doc_file != None:
if not os.path.isfile(os.path.join(docs_path, f"{args.doc_file}.md")):
console.print(f"[b red]digpkg: can't read docs: the mineral [i]{args.mineral_name}[/] has no doc file named \"{args.doc_file}\".")
sys.exit(1)
os.chdir(docs_path)
app = DocsApp(f"{args.doc_file}.md")
app.run()
sys.exit(0)
if os.path.isfile(summary_md_file):
os.chdir(mineral_path)
app = DocsApp(summary_md_file)
app.run()
sys.exit(0)
else:
console.print(f"[b red]digpkg: can't read docs: the mineral [i]{args.mineral_name}[/] has no file named SUMMARY.md, please use the [i]--doc-file[/] argument to specify a specific doc to open instead.")
sys.exit(1)

View File

@@ -3,19 +3,23 @@ import os
import sys
import tempfile
import tarfile
import configparser
import shutil
from rich.console import Console
from rich.progress import SpinnerColumn
from util import check_ground_libs_path, check_sudo
from .util import check_ground_libs_path, check_sudo
console = Console()
def install_package(package_name, version, args):
def install_package(package_name, version, args, is_dependency: bool = False):
retries_left = args.max_retries
with console.status("Downloading tarball...", spinner="bouncingBall", spinner_style="blue") as status:
console.print(f"Installing {package_name} [d]({version})[/]")
while retries_left > 0:
# grab the tar ball
response = requests.get(f"https://chookspace.com/api/packages/ground/generic/{package_name}/{version}/mineral.tar")
@@ -65,12 +69,32 @@ def install_package(package_name, version, args):
console.status("Finishing up...")
package_folder = os.path.join(extract_dir, f"{package_name}/")
if not os.path.isfile(os.path.join(package_folder, "mineral.ini")):
console.print(f"[b red]digpkg: failed to install {package_name}: the mineral doesn't have a mineral.ini file, please contact the maintainer because they've set up the mineral incorrectly.")
console.print("[d]Cleaning up failed install...[/]")
shutil.rmtree(os.path.join(extract_dir, package_name))
sys.exit(1)
config_parser = configparser.ConfigParser()
config_parser.read(os.path.join(package_folder, "mineral.ini"))
dependencies = config_parser["dependencies"]
# create a symlink from the main.so file to the ground libs folder so ground can find it
symlink_path = os.path.join(extract_dir, f"{package_name}.so") # the path where the symlink is
if not os.path.isfile(symlink_path):
os.symlink(os.path.join(extract_dir, package_name, "main.so"), symlink_path)
console.print("[:white_check_mark:] Done!")
if is_dependency:
console.print(f"[d][:white_check_mark:] Done installing dependency: {package_name}[/]")
for dependency, dependency_version in dependencies.items():
install_package(dependency, dependency_version, args, True)
if not is_dependency:
console.print(f"\n[b green][:white_check_mark:] Installed {package_name}![/]")
def install(args):
@@ -80,10 +104,13 @@ def install(args):
for package in args.names:
# figure out which version to install
package_name = package
version = "latest"
version = "1.0.0"
if "@" in package:
split = package.split("@")
package_name = split[0]
version = split[1]
else:
console.print(f"[b red]digpkg: failed to install package: please specify version to install for {package_name}[/]")
sys.exit(1)
install_package(package_name, version, args)

View File

@@ -3,14 +3,17 @@ import configparser
from rich import print
from rich.table import Table
from util import check_ground_libs_path
from .util import check_ground_libs_path
def list_cmd(args):
check_ground_libs_path()
ground_libs_folder = os.getenv("GROUND_LIBS")
folders = os.listdir(ground_libs_folder)
folders = []
if os.path.isdir(ground_libs_folder): # used to prevent errors that are caused by the GROUND_LIBS folder not existing
folders = os.listdir(ground_libs_folder)
table = Table("Name", "Version", "Description", title="Installed")
config_parser = configparser.ConfigParser()

View File

@@ -1,11 +1,13 @@
#!/usr/bin/env python3
import argparse
import os, sys
from install import install
from publish import publish
from remove import remove
from list import list_cmd
from uninstall import uninstall
from .install import install
from .publish import publish
from .remove import remove
from .list import list_cmd
from .uninstall import uninstall
from .build import build
def parse_arguments():
@@ -27,15 +29,25 @@ def parse_arguments():
# publish command
publish_command = sub_parsers.add_parser(name="publish", description="publish a package to the repository")
publish_command.add_argument("name", help="name and version of the package")
#publish_command.add_argument("name", help="name and version of the package")
publish_command.add_argument("folder_path", help="path to the folder that will be uploaded")
# remove command
remove_command = sub_parsers.add_parser(name="remove", description="remove a published package from the repository")
remove_command.add_argument("name", help="name and version of the package")
# env command
# build command
build_command = sub_parsers.add_parser(name="build", description="build a folder as a mineral and either install it or prepare it for publishing")
build_command.add_argument("folder_path", help="path to the folder to build")
build_command.add_argument("--gcc-args", nargs="*", help="any extra args you want to give to gcc")
build_command.add_argument("--package", action="store_true", help="generate a folder with a mineral.ini and all the other files you need to publish the package")
# docs command
docs_command = sub_parsers.add_parser(name="docs", description="read the docs of a mineral")
docs_command.add_argument("mineral_name", help="name of the mineral you want to read the docs of")
docs_command.add_argument("-d", "--doc-file", help="load a specific doc file")
# parse arguments are run the command we chose
args = arg_parser.parse_args()
if not args.command:
@@ -52,6 +64,11 @@ def parse_arguments():
list_cmd(args)
elif args.command == "uninstall":
uninstall(args)
elif args.command == "build":
build(args)
elif args.command == "docs":
from .docs import docs
docs(args)
def main():
parse_arguments()

View File

@@ -1,3 +1,4 @@
import configparser
import tarfile
import tempfile
import os, sys
@@ -13,13 +14,7 @@ console = Console()
def publish(args):
if not "@" in args.name:
console.print(f"[b red]digpkg: failed to publish mineral: please include the version number in the package name. e.g: request@1.0.0")
sys.exit(1)
split_name = args.name.split("@")
mineral_name = split_name[0]
version = split_name[1]
mineral_name = os.path.basename(args.folder_path)
# sanity checks
if not os.path.isdir(args.folder_path):
@@ -28,6 +23,12 @@ def publish(args):
if not os.path.isfile(os.path.join(args.folder_path, "mineral.ini")):
console.print(f"[b red]digpkg: failed to publish mineral: mineral has no \"mineral.ini\" file")
sys.exit(1)
if os.path.basename(os.path.normpath(args.folder_path)).endswith("_build"):
console.print(f"\n[b yellow]You didn't remove the \"_build\" suffix from your mineral's folder name!\n\nIf this is intentional you can ignore this message, however it is bad practice.\nIf this is not intentional, you will be unable to install your package properly using dig.[/]")
config_parser = configparser.ConfigParser()
config_parser.read(os.path.join(args.folder_path, "mineral.ini"))
version = config_parser["package"]["version"]
# ask for user and pass
console.print("[b]Please authenticate.\n[/]")
@@ -53,7 +54,7 @@ def publish(args):
sys.exit(1)
# compress to a tar file
console.status("Compressing")
console.status("Compressing", spinner_style="green")
f = tempfile.TemporaryFile(mode="wb+")
with tarfile.open(fileobj=f, mode="w:gz") as tar_file:
tar_file.add(args.folder_path, arcname=os.path.basename(args.folder_path))
@@ -63,7 +64,7 @@ def publish(args):
console.print("[d][:white_check_mark:] Compressed![/]")
# send the request
status.update("Uploading...")
status.update("Uploading...", spinner_style="blue")
response = requests.put(
url=f"https://chookspace.com/api/packages/ground/generic/{mineral_name}/{version}/mineral.tar",
data=f,

View File

@@ -1,7 +1,7 @@
import os, sys
import shutil
from util import check_ground_libs_path, check_sudo
from .util import check_ground_libs_path, check_sudo
from rich.console import Console
@@ -12,7 +12,7 @@ def uninstall(args):
check_sudo()
check_ground_libs_path()
with console.status(status=f"Looking for [i]{args.name}[/]...", spinner="bouncingBall", spinner_style="blue") as status:
with console.status(status=f"Looking for [i]{args.name}[/]...", spinner="bouncingBall", spinner_style="green") as status:
mineral_path = os.path.join(os.getenv("GROUND_LIBS"), args.name)
symlink_path = os.path.join(os.getenv("GROUND_LIBS"), f"{args.name}.so")

View File

@@ -6,7 +6,7 @@ from rich import print
def check_ground_libs_path():
# ensure the GROUND_LIBS var is set
if not os.getenv("GROUND_LIBS"):
print("digpkg: the [i]GROUND_LIBS[/] environment variable is not set, defaulting to /usr/lib/ground/")
print("[d]digpkg: the [i]GROUND_LIBS[/] environment variable is not set, defaulting to /usr/lib/ground/")
os.environ["GROUND_LIBS"] = "/usr/lib/ground/"
def check_sudo():

BIN
mineral.tar Normal file

Binary file not shown.

View File

@@ -1,2 +1,3 @@
rich
requests
setuptools
wheel
twine

17
setup.py Normal file
View File

@@ -0,0 +1,17 @@
from setuptools import setup, find_packages
setup(
name="digpkg",
version="1.1",
packages=find_packages(),
install_requires=[
"textual>=7.3.0",
"requests>=2.32.5",
"textual-autocomplete>=4.0.6"
],
entry_points={
"console_scripts": [
"dig = dig:main"
]
}
)

4
test.grnd Normal file
View File

@@ -0,0 +1,4 @@
extern "math"
call !math_RandomDouble 1.0 10.0 &var
println $var