commit 37b67bc94dd6bdd14bd5fa86c3d308ad69402d86
parent f47768c7176b8600b9a3ad99fb3c22c709bf8df1
Author: finwo <finwo@pm.me>
Date: Sat, 14 Mar 2026 23:38:06 +0100
Merge pull request #16 from finwo/rewrite-c
Full rewrite, simpler repo and config storage
Diffstat:
39 files changed, 2749 insertions(+), 1361 deletions(-)
diff --git a/.clang-format b/.clang-format
@@ -0,0 +1,334 @@
+---
+Language: Cpp
+AccessModifierOffset: -1
+AlignAfterOpenBracket: Align
+AlignArrayOfStructures: None
+AlignConsecutiveAssignments:
+ Enabled: true
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: true
+AlignConsecutiveBitFields:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveDeclarations:
+ Enabled: true
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: true
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveMacros:
+ Enabled: true
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveShortCaseStatements:
+ Enabled: true
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCaseArrows: false
+ AlignCaseColons: false
+AlignConsecutiveTableGenBreakingDAGArgColons:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveTableGenCondOperatorColons:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveTableGenDefinitionColons:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignEscapedNewlines: Left
+AlignOperands: Align
+AlignTrailingComments:
+ Kind: Always
+ OverEmptyLines: 0
+AllowAllArgumentsOnNextLine: true
+AllowAllParametersOfDeclarationOnNextLine: true
+AllowBreakBeforeNoexceptSpecifier: Never
+AllowShortBlocksOnASingleLine: Never
+AllowShortCaseExpressionOnASingleLine: true
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortCompoundRequirementOnASingleLine: true
+AllowShortEnumsOnASingleLine: true
+AllowShortFunctionsOnASingleLine: None
+AllowShortIfStatementsOnASingleLine: WithoutElse
+AllowShortLambdasOnASingleLine: All
+AllowShortLoopsOnASingleLine: true
+AllowShortNamespacesOnASingleLine: false
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakBeforeMultilineStrings: true
+AttributeMacros:
+ - __capability
+ - absl_nonnull
+ - absl_nullable
+ - absl_nullability_unknown
+BinPackArguments: true
+BinPackLongBracedList: true
+BinPackParameters: BinPack
+BitFieldColonSpacing: Both
+BracedInitializerIndentWidth: -1
+BraceWrapping:
+ AfterCaseLabel: false
+ AfterClass: false
+ AfterControlStatement: Never
+ AfterEnum: false
+ AfterExternBlock: false
+ AfterFunction: true
+ AfterNamespace: false
+ AfterObjCDeclaration: false
+ AfterStruct: false
+ AfterUnion: false
+ BeforeCatch: false
+ BeforeElse: false
+ BeforeLambdaBody: false
+ BeforeWhile: false
+ IndentBraces: false
+ SplitEmptyFunction: true
+ SplitEmptyRecord: true
+ SplitEmptyNamespace: true
+BreakAdjacentStringLiterals: true
+BreakAfterAttributes: Leave
+BreakAfterJavaFieldAnnotations: false
+BreakAfterReturnType: None
+BreakArrays: true
+BreakBeforeBinaryOperators: None
+BreakBeforeConceptDeclarations: Always
+BreakBeforeBraces: Attach
+BreakBeforeInlineASMColon: OnlyMultiline
+BreakBeforeTemplateCloser: false
+BreakBeforeTernaryOperators: true
+BreakBinaryOperations: Never
+BreakConstructorInitializers: BeforeColon
+BreakFunctionDefinitionParameters: false
+BreakInheritanceList: BeforeColon
+BreakStringLiterals: true
+BreakTemplateDeclarations: Yes
+ColumnLimit: 120
+CommentPragmas: '^ IWYU pragma:'
+CompactNamespaces: false
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DerivePointerAlignment: false
+DisableFormat: false
+EmptyLineAfterAccessModifier: Never
+EmptyLineBeforeAccessModifier: LogicalBlock
+EnumTrailingComma: Leave
+ExperimentalAutoDetectBinPacking: false
+FixNamespaceComments: true
+ForEachMacros:
+ - foreach
+ - Q_FOREACH
+ - BOOST_FOREACH
+IfMacros:
+ - KJ_IF_MAYBE
+IncludeBlocks: Regroup
+IncludeCategories:
+ - Regex: '^<ext/.*\.h>'
+ Priority: 2
+ SortPriority: 0
+ CaseSensitive: false
+ - Regex: '^<.*\.h>'
+ Priority: 1
+ SortPriority: 0
+ CaseSensitive: false
+ - Regex: '^<.*'
+ Priority: 2
+ SortPriority: 0
+ CaseSensitive: false
+ - Regex: '.*'
+ Priority: 3
+ SortPriority: 0
+ CaseSensitive: false
+IncludeIsMainRegex: '([-_](test|unittest))?$'
+IncludeIsMainSourceRegex: ''
+IndentAccessModifiers: false
+IndentCaseBlocks: true
+IndentCaseLabels: true
+IndentExportBlock: true
+IndentExternBlock: AfterExternBlock
+IndentGotoLabels: true
+IndentPPDirectives: None
+IndentRequiresClause: true
+IndentWidth: 2
+IndentWrappedFunctionNames: false
+InsertBraces: false
+InsertNewlineAtEOF: true
+InsertTrailingCommas: None
+IntegerLiteralSeparator:
+ Binary: 0
+ BinaryMinDigits: 0
+ Decimal: 0
+ DecimalMinDigits: 0
+ Hex: 0
+ HexMinDigits: 0
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLines:
+ AtEndOfFile: false
+ AtStartOfBlock: false
+ AtStartOfFile: false
+KeepFormFeed: false
+LambdaBodyIndentation: Signature
+LineEnding: DeriveLF
+MacroBlockBegin: ''
+MacroBlockEnd: ''
+MainIncludeChar: Quote
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBinPackProtocolList: Never
+ObjCBlockIndentWidth: 2
+ObjCBreakBeforeNestedBlockParam: true
+ObjCSpaceAfterProperty: false
+ObjCSpaceBeforeProtocolList: true
+OneLineFormatOffRegex: ''
+PackConstructorInitializers: NextLine
+PenaltyBreakAssignment: 2
+PenaltyBreakBeforeFirstCallParameter: 1
+PenaltyBreakBeforeMemberAccess: 150
+PenaltyBreakComment: 300
+PenaltyBreakFirstLessLess: 120
+PenaltyBreakOpenParenthesis: 0
+PenaltyBreakScopeResolution: 500
+PenaltyBreakString: 1000
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 1000000
+PenaltyIndentedWhitespace: 0
+PenaltyReturnTypeOnItsOwnLine: 200
+PointerAlignment: Right
+PPIndentWidth: -1
+QualifierAlignment: Leave
+RawStringFormats:
+ - Language: Cpp
+ Delimiters:
+ - cc
+ - CC
+ - cpp
+ - Cpp
+ - CPP
+ - 'c++'
+ - 'C++'
+ CanonicalDelimiter: ''
+ BasedOnStyle: google
+ - Language: TextProto
+ Delimiters:
+ - pb
+ - PB
+ - proto
+ - PROTO
+ EnclosingFunctions:
+ - EqualsProto
+ - EquivToProto
+ - PARSE_PARTIAL_TEXT_PROTO
+ - PARSE_TEST_PROTO
+ - PARSE_TEXT_PROTO
+ - ParseTextOrDie
+ - ParseTextProtoOrDie
+ - ParseTestProto
+ - ParsePartialTestProto
+ CanonicalDelimiter: pb
+ BasedOnStyle: google
+ReferenceAlignment: Pointer
+ReflowComments: Always
+RemoveBracesLLVM: false
+RemoveEmptyLinesInUnwrappedLines: false
+RemoveParentheses: Leave
+RemoveSemicolon: false
+RequiresClausePosition: OwnLine
+RequiresExpressionIndentation: OuterScope
+SeparateDefinitionBlocks: Leave
+ShortNamespaceLines: 1
+SkipMacroDefinitionBody: false
+SortIncludes:
+ Enabled: true
+ IgnoreCase: false
+SortJavaStaticImport: Before
+SortUsingDeclarations: LexicographicNumeric
+SpaceAfterCStyleCast: false
+SpaceAfterLogicalNot: false
+SpaceAfterOperatorKeyword: false
+SpaceAfterTemplateKeyword: true
+SpaceAroundPointerQualifiers: Default
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCaseColon: false
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeJsonColon: false
+SpaceBeforeParens: ControlStatements
+SpaceBeforeParensOptions:
+ AfterControlStatements: true
+ AfterForeachMacros: true
+ AfterFunctionDefinitionName: false
+ AfterFunctionDeclarationName: false
+ AfterIfMacros: true
+ AfterNot: false
+ AfterOverloadedOperator: false
+ AfterPlacementOperator: true
+ AfterRequiresInClause: false
+ AfterRequiresInExpression: false
+ BeforeNonEmptyParentheses: false
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceBeforeSquareBrackets: false
+SpaceInEmptyBlock: false
+SpacesBeforeTrailingComments: 2
+SpacesInAngles: Never
+SpacesInContainerLiterals: true
+SpacesInLineCommentPrefix:
+ Minimum: 1
+ Maximum: -1
+SpacesInParens: Never
+SpacesInParensOptions:
+ ExceptDoubleParentheses: false
+ InCStyleCasts: false
+ InConditionalStatements: false
+ InEmptyParentheses: false
+ Other: false
+SpacesInSquareBrackets: false
+Standard: Auto
+StatementAttributeLikeMacros:
+ - Q_EMIT
+StatementMacros:
+ - Q_UNUSED
+ - QT_REQUIRE_VERSION
+TableGenBreakInsideDAGArg: DontBreak
+TabWidth: 8
+UseTab: Never
+VerilogBreakBetweenInstancePorts: true
+WhitespaceSensitiveMacros:
+ - BOOST_PP_STRINGIZE
+ - CF_SWIFT_NAME
+ - NS_SWIFT_NAME
+ - PP_STRINGIZE
+ - STRINGIZE
+WrapNamespaceBodyWithEmptyLines: Leave
+...
+
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
@@ -0,0 +1,66 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - '*'
+
+jobs:
+ build:
+ runs-on: ${{ matrix.runs-on }}
+ strategy:
+ matrix:
+ include:
+ - runs-on: ubuntu-24.04
+ os: linux
+ arch: x64
+ - runs-on: ubuntu-24.04-arm
+ os: linux
+ arch: arm64
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set artifact name
+ id: artifact
+ run: |
+ ARTIFACT="dep-${{ matrix.os }}-${{ matrix.arch }}"
+ echo "name=$ARTIFACT" >> $GITHUB_OUTPUT
+
+ - name: Install dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y clang curl libcurl4-openssl-dev
+
+ - name: Build
+ run: make dep
+
+ - name: Rename binary
+ run: |
+ mv dep ${{ steps.artifact.outputs.name }}
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ steps.artifact.outputs.name }}
+ path: ${{ steps.artifact.outputs.name }}
+
+ release:
+ needs: build
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+
+ steps:
+ - name: Download all artifacts
+ uses: actions/download-artifact@v4
+ with:
+ path: artifacts
+
+ - name: Create Release
+ uses: softprops/action-gh-release@v2
+ with:
+ files: artifacts/*/*
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
@@ -1,4 +1,3 @@
-/bak/
-/util/
/lib/
-node_modules/
+*.o
+/dep
diff --git a/LICENSE b/LICENSE
@@ -1,20 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2023 finwo
-
-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/LICENSE.md b/LICENSE.md
@@ -0,0 +1,34 @@
+Copyright (c) 2026 finwo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to use, copy,
+modify, and distribute the Software, subject to the following conditions:
+
+ 1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions, and the following disclaimer.
+
+ 2. Redistributions in binary form, or any public offering of the Software
+ (including hosted or managed services), must reproduce the above copyright
+ notice, this list of conditions, and the following disclaimer in the
+ documentation and/or other materials provided.
+
+ 3. Any redistribution or public offering of the Software must clearly attribute
+ the Software to the original copyright holder, reference this License, and
+ include a link to the official project repository or website.
+
+ 4. The Software may not be renamed, rebranded, or marketed in a manner that
+ implies it is an independent or proprietary product. Derivative works must
+ clearly state that they are based on the Software.
+
+ 5. Modifications to copies of the Software must carry prominent notices stating
+ that changes were made, the nature of the modifications, and the date of the
+ modifications.
+
+Any violation of these conditions terminates the permissions granted herein.
+
+THIS 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 COPYRIGHT
+HOLDER OR CONTRIBUTORS 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/Makefile b/Makefile
@@ -1,31 +1,97 @@
+CC?=clang
+
+FIND=$(shell which gfind find | head -1)
+OBJCOPY?=objcopy
+
+UNAME_M:=$(shell uname -m)
+ARCH_TARGET?=$(shell echo $(UNAME_M) | sed -e 's/x86_64/elf64-x86-64/' -e 's/arm64/elf64-littleaarch64/' -e 's/aarch64/elf64-littleaarch64/')
+ARCH_BIN?=$(shell echo $(UNAME_M) | sed -e 's/x86_64/i386/' -e 's/arm64/aarch64/' -e 's/aarch64/aarch64/')
+
SRC:=
-SRC+=$(wildcard src/*.sh)
-SRC+=$(wildcard src/*.txt)
-SRC+=$(wildcard src/*/*.sh)
-SRC+=$(wildcard src/*/*.txt)
-SRC+=$(wildcard src/*/*/*.sh)
-SRC+=$(wildcard src/*/*/*.txt)
-
-PREPROCESS=preprocess --substitute
+INCLUDES:=
+CFLAGS:=
+LDFLAGS:=
DESTDIR?=/usr/local
-TARGET=dep
-default: dist/$(TARGET) README.md
+SRC+=$(shell $(FIND) src/ -type f -name '*.c')
+INCLUDES+=-Isrc
+INCLUDES+=-Ilib/.dep/include
-dist/$(TARGET): $(SRC)
- mkdir -p $(shell dirname $@)
- echo '#!/usr/bin/env bash' > "$@"
- $(PREPROCESS) -D __NAME=$(TARGET) -I src src/main.sh | tee -a $@ > /dev/null
- chmod +x "$@"
+LIBS:=
-.PHONY: install
-install: dist/$(TARGET)
- install "dist/$(TARGET)" "$(DESTDIR)/bin"
+LIBS+=lib/cofyc/argparse
+SRC+=lib/cofyc/argparse/argparse.c
+
+LIBS+=lib/emmanuel-marty/em_inflate
+SRC+=lib/emmanuel-marty/em_inflate/lib/em_inflate.c
+
+LIBS+=lib/erkkah/naett
+SRC+=lib/erkkah/naett/naett.c
+LDFLAGS+=-lcurl -lpthread
+
+LIBS+=lib/rxi/microtar
+SRC+=lib/rxi/microtar/src/microtar.c
+
+LIBS+=lib/tidwall/json.c
+SRC+=lib/tidwall/json.c/json.c
+
+OBJ:=$(SRC:.c=.o)
+OBJ:=$(OBJ:.cc=.o)
+OBJ+=license.o
+
+CFLAGS+=${INCLUDES}
+
+.PHONY: default
+default: dep
-README.md: README.md.html
- $(PREPROCESS) -D __NAME=$(TARGET) $< > "$@"
+license.o: LICENSE.md
+ $(OBJCOPY) --input binary --output $(ARCH_TARGET) --binary-architecture $(ARCH_BIN) $< $@
+
+lib/cofyc/argparse:
+ mkdir -p lib/cofyc/argparse
+ curl -sL https://github.com/cofyc/argparse/archive/refs/heads/master.tar.gz | tar xzv --strip-components=1 -C lib/cofyc/argparse
+ mkdir -p lib/.dep/include/cofyc
+ ln -s ../../../cofyc/argparse/argparse.h lib/.dep/include/cofyc/argparse.h
+
+lib/emmanuel-marty/em_inflate:
+ mkdir -p lib/emmanuel-marty/em_inflate
+ curl -sL https://github.com/emmanuel-marty/em_inflate/archive/refs/heads/master.tar.gz | tar xzv --strip-components=1 -C lib/emmanuel-marty/em_inflate
+ mkdir -p lib/.dep/include/emmanuel-marty
+ ln -s ../../../emmanuel-marty/em_inflate/lib/em_inflate.h lib/.dep/include/emmanuel-marty/em_inflate.h
+
+lib/erkkah/naett:
+ mkdir -p lib/erkkah/naett
+ curl -sL https://github.com/erkkah/naett/archive/refs/heads/main.tar.gz | tar xzv --strip-components=1 -C lib/erkkah/naett
+ mkdir -p lib/.dep/include/erkkah
+ ln -s ../../../erkkah/naett/naett.h lib/.dep/include/erkkah/naett.h
+
+lib/rxi/microtar:
+ mkdir -p lib/rxi/microtar
+ curl -sL https://github.com/rxi/microtar/archive/refs/heads/master.tar.gz | tar xzv --strip-components=1 -C lib/rxi/microtar
+ mkdir -p lib/.dep/include/rxi
+ ln -s ../../../rxi/microtar/src/microtar.h lib/.dep/include/rxi/microtar.h
+
+lib/tidwall/json.c:
+ mkdir -p lib/tidwall/json.c
+ curl -sL https://github.com/tidwall/json.c/archive/refs/heads/main.tar.gz | tar xzv --strip-components=1 -C lib/tidwall/json.c
+ mkdir -p lib/.dep/include/tidwall
+ ln -s ../../../tidwall/json.c/json.h lib/.dep/include/tidwall/json.h
+
+.c.o:
+ ${CC} $< ${CFLAGS} -c -o $@
+
+dep: $(LIBS) $(OBJ)
+ ${CC} ${OBJ} ${CFLAGS} ${LDFLAGS} -o dep
+ strip --strip-all dep
+
+.PHONY: install
+install: dep
+ install dep ${DESTDIR}/bin
.PHONY: clean
clean:
- rm -rf dist
- rm -f README.md
+ rm -f $(OBJ)
+
+.PHONY: format
+format:
+ $(FIND) src/ -type f \( -name '*.c' -o -name '*.h' \) -exec clang-format -i {} +
diff --git a/README.md b/README.md
@@ -1,13 +1,13 @@
dep
-======
+===
-General purpose dependency manager, with a slight focus on static C libraries
+General purpose dependency manager for embedded C libraries, written in C.
Summary
-------
```
-Usage: dep [global options] <command> [options] [-- ...args]
+Usage: dep [global options] <command> [command options]
Global options:
n/a
@@ -15,92 +15,164 @@ Global options:
Commands:
a(dd) Add a new dependency to the project
i(nstall) Install all the project's dependencies
- h(elp) [topic] Show this help or the top-level info about a command
+ init Initialize a new project with a .dep file
+ license Show license information
r(epo(sitory)) Repository management
+ help [topic] Show this help or the top-level info about a command
Help topics:
global This help text
add More detailed explanation on the add command
install More detailed explanation on the install command
+ init More detailed explanation on the init command
repository More detailed explanation on the repository command
```
Installation
------------
-To install dep, simply download [dist/dep](dist/dep) and place it in your
-`/usr/local/bin` directory or anywhere else that's included in your `$PATH`.
+**Note: Windows is not supported.** This project only targets posix-compliant operating systems.
-By default, no default repositories are enabled, so it's advisable to run the
-following to enable the official repository:
+To install dep, build it from source and place the binary in your `$PATH`:
```sh
-dep repository add finwo https://github.com/finwo/dep-repository/archive/refs/heads/main.tar.gz
+make
+sudo make install
```
+To install the binary in a location other than `/usr/local/bin`, pass the
+`DESTDIR` definition to the `make install` command (default: `/usr/local`).
+
+By default, no default repositories are enabled. You'll need to add your own
+or use GitHub directly.
+
Usage
-----
-#### Update repositories
+### Initializing a project
+
+To start using dep in your project, run the init command to create a `.dep` file:
+
+```sh
+dep init
+```
+
+This creates an empty `.dep` file in the current directory. You can also specify
+a target directory:
+
+```sh
+dep init /path/to/project
+```
+
+### Adding a dependency
+
+To add a package, you can call the following command:
+
+```sh
+dep add owner/library
+dep add owner/library version
+```
+
+If a version is not specified, the latest version from the repository will be
+used, or the default branch from GitHub.
+
+Examples:
+
+```sh
+dep add finwo/palloc # Latest from repo or GitHub main branch
+dep add finwo/palloc edge # Specific version/branch
+dep add finwo/palloc v1.0.0 # Specific tag
+```
+
+You can also add a dependency with a direct URL:
-dep keeps local cache of the repositories you have enabled. To update this
-cache, run the following command
+```sh
+dep add mylib https://example.com/mylib.tar.gz
+```
+
+### Installing dependencies
+
+To install all dependencies listed in your `.dep` file:
```sh
-dep repository update
+dep install
```
-#### Adding a dependency to your project
+Dependencies are installed to the `lib/` directory by default.
-To add a package, you can call the following command to install a specific
-version of the package:
+### Repository management
+
+dep can use custom repositories to discover packages. Repositories are
+configured in `~/.config/finwo/dep/repositories.d/`.
+
+To add a repository:
```sh
-dep add package/identifier@version
+dep repository add myorg https://example.com/path/to/manifest
```
-If you have package.channel set in your project's package.ini, you can also
-leave the version from the command to automatically select the version you've
-set there.
+To list configured repositories:
-```ini
-[package]
-channel=edge
+```sh
+dep repository list
```
+To remove a repository:
+
```sh
-dep add package/identifier
+dep repository remove myorg
```
-For example, if you'd want to install the [finwo/palloc][palloc] package, you
-could use the following comamnd:
+To clean the cache of downloaded repository manifests:
```sh
-dep add finwo/palloc@edge # with version specifier
-dep add finwo/palloc # without version specifier
+dep repository clean-cache
```
+Creating Repositories
+--------------------
+
+Anyone can create a repository to host their own package manifests. A
+repository is simply a static file server hosting a manifest file.
+
+The manifest is a text file with one package per line. Lines starting with
+`#` are comments. Each line has the following format:
+
+```
+name@version url
+```
+
+The version is optional. If omitted, the package is available without a
+specific version.
+
+Example manifest:
+
+```
+# My organization's packages
+finwo/palloc@edge https://github.com/finwo/palloc/archive/refs/heads/edge.tar.gz
+finwo/palloc@v1.0.0 https://github.com/finwo/palloc/archive/refs/tags/v1.0.0.tar.gz
+finwo/palloc https://github.com/finwo/palloc/archive/refs/heads/main.tar.gz
+myorg/mylib https://example.com/mylib-v1.0.0.tar.gz
+myorg/mylib@2.0.0 https://example.com/mylib-v2.0.0.tar.gz
+```
+
+Host the manifest file on any static file server (GitHub Pages, S3, nginx,
+etc.) and add the URL using `dep repository add`.
+
Building
--------
-Building this dependency manager requires
-[preprocess](https://pypi.org/project/preprocess/) which you can install by
-running `pip install preprocess`.
+Building this dependency manager requires:
+- clang (or your preferred C compiler)
+- make
-After fetching the preprocess dependency, you can build & install dep by running
-the following commands:
+Simply run:
```sh
make
-sudo make install
```
-To install the binary in a location other than `/usr/local/bin`, pass the
-`DESTDIR` definition to the `make install` command (default: `/usr/local`).
-
License
-------
-This project falls under the [MIT license](LICENSE)
-
-[palloc]: https://github.com/finwo/palloc.c
+This project falls under the [FGPL license](LICENSE.md)
diff --git a/README.md.html b/README.md.html
@@ -1,91 +0,0 @@
-__NAME
-======
-
-General purpose dependency manager, with a slight focus on static C libraries
-
-Summary
--------
-
-```
-<!-- #include "src/command/help/topic/global.txt" -->
-```
-
-Installation
-------------
-
-To install __NAME, simply download [dist/dep](dist/dep) and place it in your
-`/usr/local/bin` directory or anywhere else that's included in your `$PATH`.
-
-By default, no default repositories are enabled, so it's advisable to run the
-following to enable the official repository:
-
-```sh
-__NAME repository add finwo https://github.com/finwo/dep-repository/archive/refs/heads/main.tar.gz
-```
-
-Usage
------
-
-#### Update repositories
-
-__NAME keeps local cache of the repositories you have enabled. To update this
-cache, run the following command
-
-```sh
-__NAME repository update
-```
-
-#### Adding a dependency to your project
-
-To add a package, you can call the following command to install a specific
-version of the package:
-
-```sh
-__NAME add package/identifier@version
-```
-
-If you have package.channel set in your project's package.ini, you can also
-leave the version from the command to automatically select the version you've
-set there.
-
-```ini
-[package]
-channel=edge
-```
-
-```sh
-__NAME add package/identifier
-```
-
-For example, if you'd want to install the [finwo/palloc][palloc] package, you
-could use the following comamnd:
-
-```sh
-__NAME add finwo/palloc@edge # with version specifier
-__NAME add finwo/palloc # without version specifier
-```
-
-Building
---------
-
-Building this dependency manager requires
-[preprocess](https://pypi.org/project/preprocess/) which you can install by
-running `pip install preprocess`.
-
-After fetching the preprocess dependency, you can build & install dep by running
-the following commands:
-
-```sh
-make
-sudo make install
-```
-
-To install the binary in a location other than `/usr/local/bin`, pass the
-`DESTDIR` definition to the `make install` command (default: `/usr/local`).
-
-License
--------
-
-This project falls under the [MIT license](LICENSE)
-
-[palloc]: https://github.com/finwo/palloc.c
diff --git a/action.yml b/action.yml
@@ -2,15 +2,56 @@ name: Setup DEP package manager
description: Installs the DEP package manager into the runner
runs:
- using: "composite"
+ using: composite
steps:
+ - name: Determine platform artifact name
+ id: platform
+ shell: bash
+ run: |
+ OS=$(echo "${{ runner.os }}" | tr '[:upper:]' '[:lower:]')
+ ARCH=$(echo "${{ runner.arch }}" | tr '[:upper:]' '[:lower:]')
- - name: Extend executable path
+ ARTIFACT_NAME="dep-${OS}-${ARCH}"
+
+ echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
+
+ - name: Download prebuilt binary
+ id: download
shell: bash
- run: echo "${GITHUB_ACTION_PATH}/dist" >> $GITHUB_PATH
+ run: |
+ TAG="edge"
+ REPO="finwo/dep"
+ ARTIFACT="${{ steps.platform.outputs.artifact_name }}"
+
+ mkdir -p "${GITHUB_ACTION_PATH}/dist"
+
+ DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${TAG}/${ARTIFACT}"
- - name: Install default repositories
+ if curl -sfL "$DOWNLOAD_URL" -o "${GITHUB_ACTION_PATH}/dist/dep"; then
+ echo "Downloaded $ARTIFACT from release $TAG"
+ echo "status=success" >> $GITHUB_OUTPUT
+ else
+ echo "Prebuilt binary not found for $ARTIFACT in release $TAG, will build from source"
+ echo "status=fallback" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Build from source (fallback)
+ if: steps.download.outputs.status == 'fallback'
shell: bash
run: |
- dep repository add finwo 'https://github.com/finwo/dep-repository/archive/refs/heads/main.tar.gz'
- dep repository update
+ echo "Building dep from source..."
+
+ cd "$GITHUB_ACTION_PATH"
+ make clean || true
+ make dep
+
+ mkdir -p "${GITHUB_ACTION_PATH}/dist"
+ mv dep "${GITHUB_ACTION_PATH}/dist/dep"
+
+ - name: Make executable
+ shell: bash
+ run: chmod +x "${GITHUB_ACTION_PATH}/dist/dep"
+
+ - name: Extend executable path
+ shell: bash
+ run: echo "${GITHUB_ACTION_PATH}/dist" >> $GITHUB_PATH
diff --git a/dist/dep b/dist/dep
@@ -1,577 +0,0 @@
-#!/usr/bin/env bash
-cmds=("")
-declare -A help_topics
-
-read -r -d '' help_topics[global] <<- EOF
-Usage: dep [global options] <command> [options] [-- ...args]
-
-Global options:
- n/a
-
-Commands:
- a(dd) Add a new dependency to the project
- i(nstall) Install all the project's dependencies
- h(elp) [topic] Show this help or the top-level info about a command
- r(epo(sitory)) Repository management
-
-Help topics:
- global This help text
- add More detailed explanation on the add command
- install More detailed explanation on the install command
- repository More detailed explanation on the repository command
-EOF
-
-HELP_TOPIC=global
-function arg_h {
- arg_help "$@"
- return $?
-}
-function arg_help {
- if [[ $# -gt 0 ]]; then
- HELP_TOPIC=$1
- fi
- shift
-}
-
-function cmd_h {
- cmd_help "$@"
- return $?
-}
-function cmd_help {
- if [ -z "${help_topics[$HELP_TOPIC]}" ]; then
- echo "Unknown topic: $HELP_TOPIC" >&2
- exit 1
- fi
-
- echo -e "\n${help_topics[$HELP_TOPIC]}\n"
-}
-
-cmds[${#cmds[*]}]="h"
-cmds[${#cmds[*]}]="help"
-
-
-# Required for the whitespace trimming
-shopt -s extglob
-
-# None
-
-# Arguments:
-# $0 <fn_keyHandler> <str_filename> [section[.key]]
-function ini_foreach {
-
- # No file = no data
- inifile="${2}"
- if [[ ! -f "$inifile" ]]; then
- exit 1
- fi
-
- # Process the file line-by-line
- SECTION=
- while read line; do
-
- # Fix newlines
- line=$(echo $line | tr -d '\015')
-
- # Remove surrounding whitespace
- line=${line##*( )} # From the beginning
- line=${line%%*( )} # From the end
-
- # Remove comments and empty lines
- if [[ "${line:0:1}" == '#' ]] || [[ "${line:0:1}" == ';' ]] || [[ "${#line}" == 0 ]]; then
- continue
- fi
-
- # Handle section markers
- if [[ "${line:0:1}" == "[" ]]; then
- SECTION=$(echo $line | sed -e 's/\[\(.*\)\]/\1/')
- SECTION=${SECTION##*( )}
- SECTION=${SECTION%%*( )}
- SECTION="${SECTION}."
- continue
- fi
-
- # Output found variable
- NAME=${line%%=*}
- NAME=${NAME%%*( )}
- VALUE=${line#*=}
- VALUE=${VALUE##*( )}
-
- # Output searched or all
- if [[ -z "${3}" ]]; then
- $1 "$SECTION" "$NAME" "${VALUE}"
- elif [[ "${SECTION}" == "${3}" ]] || [[ "${SECTION}${NAME}" == "${3}" ]]; then
- $1 "$SECTION" "$NAME" "${VALUE}"
- fi
-
- done < "${inifile}"
-}
-
-function ini_write {
- PREVIOUSSECTION=
- echo -en "" > "$1"
- while read line; do
- KEYFULL=${line%%=*}
- VALUE=${line#*=}
- SECTION=${KEYFULL%%.*}
- KEY=${KEYFULL#*.}
- if [[ "${SECTION}" != "${PREVIOUSSECTION}" ]]; then
- if [ ! -z "${PREVIOUSSECTION}" ]; then
- echo "" >> "$1"
- fi
- echo "[${SECTION}]" >> "$1"
- PREVIOUSSECTION="${SECTION}"
- fi
- echo "${KEY}=${VALUE}" >> "$1"
- done < <(sort --unique)
-}
-
-function ini_output_full {
- echo "$1$2=$3"
-}
-function ini_output_section {
- echo "$2=$3"
-}
-function ini_output_value {
- echo "$3"
-}
-
-# Allow this file to be called stand-alone
-# ini.sh <filename> [section[.key]] [sectionmode]
-if [ $(basename $0) == "ini.sh" ]; then
- fullMode=full
- sectionMode=value
- if [[ ! -z "${3}" ]]; then
- fullMode=${3}
- sectionMode=${3}
- fi
- if [[ -z "${2}" ]]; then
- ini_foreach ini_output_${fullMode} "$@"
- else
- ini_foreach ini_output_${sectionMode} "$@"
- fi
-fi
-
-# None
-
-read -r -d '' help_topics[add] <<- EOF
-Usage: dep [global options] add <name> <url>
-
-Description:
-
- The add command will add a dependency to your package.ini and trigger the
- install command to do the actual install.
-
-Arguments:
-
- name The name of the dependency that will be installed. This will be used as
- the target directory within lib as well.
-
- url The url pointing to the package.ini that describes the dependency.
-EOF
-
-CMD_ADD_ARGS=
-
-function arg_a {
- arg_add "$@"
- return $?
-}
-function arg_add {
- CMD_ADD_ARGS=("$@")
- return 0
-}
-
-function cmd_a {
- cmd_add "$@"
- return $?
-}
-function cmd_add {
- OLD_PKG=$(ini_foreach ini_output_full "package.ini")
-
- # TODO: Assume package name is github repo if missing?
-
- # Get target & main package file
- PKG=(${CMD_ADD_ARGS[0]//@/ })
- PKGINIB="${HOME}/.config/finwo/dep/packages/${PKG[0]}/package.ini"
- if [ ! -f "${PKGINIB}" ]; then
- echo "Package not found. Did you update your repositories?" >&2
- exit 1
- fi
-
- # Get the version to add
- if [ -z "${PKG[1]}" ]; then PKG[1]=$(ini_foreach ini_output_value "${PKGINIB}" "package.channel" | tail -n 1); fi
- if [ -z "${PKG[1]}" ]; then PKG[1]=$(ini_foreach ini_output_value "package.ini" "package.channel" | tail -n 1); fi
- if [ -z "${PKG[1]}" ]; then
- echo "Could not determine desired package version. Set 'package.channel' in your package.ini to select a fallback or use 'dep repository add ${PKG[0]}@<version>' to select a specific version." >&2
- exit 1
- fi
-
- # Fetch the version-specific ini
- PKGINIV="${HOME}/.config/finwo/dep/packages/${PKG[0]}/${PKG[1]}/package.ini"
- PKGINI=
- if [ -f "${PKGINIB}" ]; then PKGINI="${PKGINIB}"; fi
- if [ -f "${PKGINIV}" ]; then PKGINI="${PKGINIV}"; fi
-
- # Extension: Check release/branch on github
- PKGGH=$(ini_foreach ini_output_value "${PKGINI}" "repository.github")
- if [ ! -z "${PKGGH}" ]; then
- URL_TAG="https://codeload.github.com/${PKGGH}/tar.gz/refs/tags/${PKG[1]}"
- URL_BRANCH="https://codeload.github.com/${PKGGH}/tar.gz/refs/heads/${PKG[1]}"
- CODE_TAG=$(curl -X HEAD --fail --dump-header - -o /dev/null "${URL_TAG}" 2>/dev/null | head -1 | awk '{print $2}')
- CODE_BRANCH=$(curl -X HEAD --fail --dump-header - -o /dev/null "${URL_BRANCH}" 2>/dev/null | head -1 | awk '{print $2}')
- if [ "${CODE_TAG}" != "200" ] && [ "${CODE_BRANCH}" != "200" ]; then
- echo "No release or branch '${PKG[1]}' found in the github repository ${PKGGH}." >&2
- echo "Check https://github.com/${PKGGH} to see the available releases and branches" >&2
- exit 1
- fi
- fi
-
- # Add the package to the dependencies
- (echo "dependencies.${PKG[0]}=${PKG[1]}" ; echo -e "${OLD_PKG}") | ini_write "package.ini"
- echo "Added to your package.ini: ${PKG[0]}@${PKG[1]}"
-}
-
-cmds[${#cmds[*]}]="a"
-cmds[${#cmds[*]}]="add"
-
-function ostype {
- case "$OSTYPE" in
- darwin*) echo "osx" ;;
- linux*) echo "lin" ;;
- bsd*) echo "bsd" ;;
- msys*) echo "win" ;;
- cygwin*) echo "win" ;;
- *) echo "unknown" ;;
- esac
-}
-
-# None
-
-read -r -d '' help_topics[install] <<- EOF
-Usage: dep [global options] install
-
-Description:
-
- The install command will iterate over all dependencies listed in your
- project's package.ini and install them 1-by-1, installing the dependency's
- dependencies as well.
-EOF
-
-CMD_INSTALL_PKG_NAME=
-CMD_INSTALL_PKG_DEST="lib"
-
-function arg_i {
- arg_install "$@"
- return $?
-}
-function arg_install {
- return 0
-}
-
-function cmd_i {
- cmd_install "$@"
- return $?
-}
-function cmd_install {
-
- # Sanity check
- PACKAGE_PATH="$(pwd)/package.ini"
- if [ ! -f "${PACKAGE_PATH}" ]; then
- echo "No package.ini in the working directory!" >&2
- exit 1
- fi
-
- # Fetch where to install dependencies
- CMD_INSTALL_PKG_DEST=$(ini_foreach ini_output_value "${PACKAGE_PATH}" "package.deps")
- if [ -z "${CMD_INSTALL_PKG_DEST}" ]; then CMD_INSTALL_PKG_DEST="lib"; fi
-
- # Reset working directory - keep cache
- rm -fr "${CMD_INSTALL_PKG_DEST}/.dep/include"
- mkdir -p "${CMD_INSTALL_PKG_DEST}/.dep/include"
- echo "INCLUDES+=-I${CMD_INSTALL_PKG_DEST}/.dep/include" > "${CMD_INSTALL_PKG_DEST}/.dep/config.mk"
- echo -n "" > "${CMD_INSTALL_PKG_DEST}/.dep/exported"
-
- # Install all dependencies
- ini_foreach cmd_install_parse_ini "${PACKAGE_PATH}"
- echo "Done"
-}
-
-cmds[${#cmds[*]}]="i"
-cmds[${#cmds[*]}]="install"
-
-declare -A CMD_INSTALL_DEPS
-function cmd_install_parse_ini {
- case "$1" in
- dependencies.)
- cmd_install_dep "$2" "$3"
- ;;
- esac
-
- # package.)
- # case "$2" in
- # name)
- # CMD_INSTALL_PKG_NAME="$3"
- # ;;
- # deps)
- # CMD_INSTALL_PKG_DEST="$3"
- # ;;
- # esac
- # ;;
- # dependencies.)
- # CMD_INSTALL_DEPS["$2"]="$3"
- # ;;
- # esac
-
-}
-
-# function cmd_install_execute {
-# cmd_install_reset_generated
-# for key in "${!CMD_INSTALL_DEPS[@]}"; do
-# cmd_install_dep "$key" "${CMD_INSTALL_DEPS[$key]}"
-# done
-# }
-
-function cmd_install_dep {
- local PKGNAME=$1
- local PKGVER=$2
-
- # Fetch versioned ini
- local PKGINIB="${HOME}/.config/finwo/dep/packages/${PKGNAME}/package.ini"
- local PKGINIV="${HOME}/.config/finwo/dep/packages/${PKGNAME}/${PKGVER}/package.ini"
- local PKGINI=
- if [ -f "${PKGINIB}" ]; then PKGINI="${PKGINIB}"; fi
- if [ -f "${PKGINIV}" ]; then PKGINI="${PKGINIV}"; fi
- if [ -z "${PKGINI}" ]; then
- echo "No package configuration found for ${PKGNAME}" >&2
- exit 1
- fi
-
- local PKG_DIR="${CMD_INSTALL_PKG_DEST}/${PKGNAME}"
- if [ -d "${PKG_DIR}" ]; then
- # Already installed, just update the pkgini ref
- PKGINI="${CMD_INSTALL_PKG_DEST}/${PKGNAME}/package.ini"
- else
- # Not installed yet, fetch code & run build steps
-
- # Copy repository's config for the package
- local PKG_SRC=$(dirname "${PKGINI}")
- mkdir -p "$(dirname "${PKG_DIR}")"
- cp -r "${PKG_SRC}" "$(dirname ${PKG_DIR})"
- PKGINI="${PKG_DIR}/package.ini"
-
- # Extended fetching detection
- local PKG_GH=$(ini_foreach ini_output_value "${PKGINI}" "repository.github")
- local PKG_TARBALL=$(ini_foreach ini_output_value "${PKGINI}" "package.src")
-
- # Fetch target tarball from github repo
- if [ ! -z "${PKG_GH}" ]; then
- URL_TAG="https://codeload.github.com/${PKG_GH}/tar.gz/refs/tags/${PKGVER}"
- URL_BRANCH="https://codeload.github.com/${PKG_GH}/tar.gz/refs/heads/${PKGVER}"
- CODE_TAG=$(curl -X HEAD --fail --dump-header - -o /dev/null "${URL_TAG}" 2>/dev/null | head -1 | awk '{print $2}')
- CODE_BRANCH=$(curl -X HEAD --fail --dump-header - -o /dev/null "${URL_BRANCH}" 2>/dev/null | head -1 | awk '{print $2}')
- if [ "${CODE_TAG}" == "200" ]; then
- PKG_TARBALL="${URL_TAG}"
- elif [ "${CODE_BRANCH}" == "200" ]; then
- PKG_TARBALL="${URL_BRANCH}"
- fi
- fi
-
- # Fetch configured or detected tarball
- if [ ! -z "${PKG_TARBALL}" ]; then
- # Downloads a tarball and extracts if over the package in our dependency directory
- # TARBALL_FILE="${HOME}/.config/finwo/dep/cache/${PKGNAME}/${PKGVER}.tar.gz"
- TARBALL_FILE="${CMD_INSTALL_PKG_DEST}/.dep/cache/${PKGNAME}/${PKGVER}.tar.gz"
- mkdir -p "$(dirname "${TARBALL_FILE}")"
- if [ ! -f "${TARBALL_FILE}" ]; then
- curl --location --progress-bar "${PKG_TARBALL}" --output "${TARBALL_FILE}"
- fi
- tar --extract --directory "${PKG_DIR}/" --strip-components 1 --file="${TARBALL_FILE}"
- fi
-
- # Install this dependency's dependencies
- while read line; do
- depname=${line%%=*}
- depver=${line#*=}
- cmd_install_dep "$depname" "$depver"
- done < <(ini_foreach ini_output_section "${PKGINI}" "dependencies." | sort --human-numeric-sort)
-
- # Handle any global build-steps defined in the package.ini
- while read line; do
- buildcmd=${line#*=}
- echo + $buildcmd
- bash -c "cd '${PKG_DIR}' ; ${buildcmd}"
- done < <(ini_foreach ini_output_section "${PKGINI}" "build." | sort --human-numeric-sort)
-
- # Handle any os-generic build-steps defined in the package.ini
- while read line; do
- buildcmd=${line#*=}
- echo + $buildcmd
- bash -c "cd '${PKG_DIR}' ; ${buildcmd}"
- done < <(ini_foreach ini_output_section "${PKGINI}" "build-$(ostype)." | sort --human-numeric-sort)
- fi
-
- # Build the package's exports
- if ! grep "${PKGNAME}" "${CMD_INSTALL_PKG_DEST}/.dep/exported" &>/dev/null ; then
- echo "${PKGNAME}" >> "${CMD_INSTALL_PKG_DEST}/.dep/exported"
- while read line; do
- filetarget=${line%%=*}
- filesource=${line#*=}
- mkdir -p "$(dirname "${CMD_INSTALL_PKG_DEST}/.dep/${filetarget}")"
- case "${filetarget}" in
- exported|cache/*)
- # Blocked
- ;;
- config.mk)
- cat "${PKG_DIR}/${filesource}" | sed "s|__DIRNAME|${PKG_DIR}|g" >> "${CMD_INSTALL_PKG_DEST}/.dep/${filetarget}"
- ;;
- *)
- ln -sf "$(pwd)/${PKG_DIR}/${filesource}" "${CMD_INSTALL_PKG_DEST}/.dep/${filetarget}"
- # cp "${PKG_DIR}/${filesource}" "${CMD_INSTALL_PKG_DEST}/.dep/${filetarget}"
- ;;
- esac
- done < <(ini_foreach ini_output_section "${PKGINI}" "export.")
- fi
-
-}
-
-read -r -d '' help_topics[repository] <<- EOF
-Usage: dep [global options] repository <command> <argument>
-
-Commands:
-
- a(dd) <name> <manifest-url> Add a repository to fetch packages from
- d(el(ete)) <name> Delete a repository
- c(lean) Remove packages cache
- u(pdate) Update packages cache
-EOF
-
-CMD_REPO_CMD=
-CMD_REPO_NAME=
-CMD_REPO_LOC=
-
-function arg_r {
- arg_repository "$@"
- return $?
-}
-function arg_repo {
- arg_repository "$@"
- return $?
-}
-function arg_repository {
- CMD_REPO_CMD=$1
-
- case "${CMD_REPO_CMD}" in
- a|add)
- CMD_REPO_NAME=$2
- CMD_REPO_LOC=$3
- ;;
- d|del|delete)
- CMD_REPO_NAME=$2
- ;;
- c|clean)
- # Intentionally empty
- ;;
- u|update)
- # Intentionally empty
- ;;
- *)
- echo "Unknown command: ${CMD_REPO_CMD}" >&2
- exit 1
- ;;
- esac
-
- return 0
-}
-
-function cmd_r {
- cmd_repository "$@"
- return $?
-}
-function cmd_repo {
- cmd_repository "$@"
- return $?
-}
-function cmd_repository {
- case "${CMD_REPO_CMD}" in
- a|add)
- mkdir -p "${HOME}/.config/finwo/dep/repositories.d"
- echo "${CMD_REPO_NAME}=${CMD_REPO_LOC}" >> "${HOME}/.config/finwo/dep/repositories.d/50-${CMD_REPO_NAME}"
- ;;
- d|del|delete)
- mkdir -p "${HOME}/.config/finwo/dep/repositories.d"
- rm -f "${HOME}/.config/finwo/dep/repositories.d/*-${CMD_REPO_NAME}"
- ;;
- c|clean)
- rm -rf "${HOME}/.config/finwo/dep/packages"
- mkdir -p "${HOME}/.config/finwo/dep/packages"
- ;;
- u|update)
- rm -rf "${HOME}/.config/finwo/dep/packages"
- mkdir -p "${HOME}/.config/finwo/dep/packages"
- mkdir -p "${HOME}/.config/finwo/dep/repositories.d"
-
- # Build complete repositories ini
- echo "" > "${HOME}/.config/finwo/dep/repositories.tmp"
- if [ -f "${HOME}/.config/finwo/dep/repositories" ]; then
- echo "[repository]" >> "${HOME}/.config/finwo/dep/repositories.tmp"
- cat "${HOME}/.config/finwo/dep/repositories" >> "${HOME}/.config/finwo/dep/repositories.tmp"
- fi
- for fname in $(ls "${HOME}/.config/finwo/dep/repositories.d/" | sort); do
- echo "[repository]" >> "${HOME}/.config/finwo/dep/repositories.tmp"
- cat "${HOME}/.config/finwo/dep/repositories.d/${fname}" >> "${HOME}/.config/finwo/dep/repositories.tmp"
- done
-
- # Download and extract them
- while read source; do
- curl --location --progress-bar "${source}" | \
- tar --gunzip --extract --directory "${HOME}/.config/finwo/dep/packages" --strip-components 1
- done < <(ini_foreach ini_output_value "${HOME}/.config/finwo/dep/repositories.tmp" "repository.")
-
- # Aannddd.. we're done with the tmp file
- rm -f "${HOME}/.config/finwo/dep/repositories.tmp"
-
- ;;
- *)
- echo "Unknown command: ${CMD_REPO_CMD}" >&2
- exit 1
- ;;
- esac
-}
-
-cmds[${#cmds[*]}]="r"
-cmds[${#cmds[*]}]="repo"
-cmds[${#cmds[*]}]="repository"
-
-function main {
- cmd=help
-
- while [ "$#" -gt 0 ]; do
-
- # If argument is a command, pass parsing on to it & stop main parser
- if [[ " ${cmds[*]} " =~ " $1 " ]]; then
- cmd=$1
- shift
- arg_$cmd "$@"
- break
- fi
-
- # Main parser
- case "$1" in
- --)
- shift
- break 2
- ;;
- *)
- echo "Unknown argument: $1" >&2
- exit 1
- ;;
- esac
- shift
-
- done
-
- cmd_$cmd
-}
-
-if [ $(basename $0) == "dep" ]; then
- main "$@"
-fi
diff --git a/package.ini b/package.ini
@@ -1,4 +0,0 @@
-[package]
-channel=edge
-deps=lib
-name=dep
diff --git a/src/command/add/help.txt b/src/command/add/help.txt
@@ -1,13 +0,0 @@
-Usage: __NAME [global options] add <name> <url>
-
-Description:
-
- The add command will add a dependency to your package.ini and trigger the
- install command to do the actual install.
-
-Arguments:
-
- name The name of the dependency that will be installed. This will be used as
- the target directory within lib as well.
-
- url The url pointing to the package.ini that describes the dependency.
diff --git a/src/command/add/index.sh b/src/command/add/index.sh
@@ -1,69 +0,0 @@
-# #include "util/ini.sh"
-
-read -r -d '' help_topics[add] <<- EOF
-# #include "help.txt"
-EOF
-
-CMD_ADD_ARGS=
-
-function arg_a {
- arg_add "$@"
- return $?
-}
-function arg_add {
- CMD_ADD_ARGS=("$@")
- return 0
-}
-
-function cmd_a {
- cmd_add "$@"
- return $?
-}
-function cmd_add {
- OLD_PKG=$(ini_foreach ini_output_full "package.ini")
-
- # TODO: Assume package name is github repo if missing?
-
- # Get target & main package file
- PKG=(${CMD_ADD_ARGS[0]//@/ })
- PKGINIB="${HOME}/.config/finwo/__NAME/packages/${PKG[0]}/package.ini"
- if [ ! -f "${PKGINIB}" ]; then
- echo "Package not found. Did you update your repositories?" >&2
- exit 1
- fi
-
- # Get the version to add
- if [ -z "${PKG[1]}" ]; then PKG[1]=$(ini_foreach ini_output_value "${PKGINIB}" "package.channel" | tail -n 1); fi
- if [ -z "${PKG[1]}" ]; then PKG[1]=$(ini_foreach ini_output_value "package.ini" "package.channel" | tail -n 1); fi
- if [ -z "${PKG[1]}" ]; then
- echo "Could not determine desired package version. Set 'package.channel' in your package.ini to select a fallback or use 'dep repository add ${PKG[0]}@<version>' to select a specific version." >&2
- exit 1
- fi
-
- # Fetch the version-specific ini
- PKGINIV="${HOME}/.config/finwo/__NAME/packages/${PKG[0]}/${PKG[1]}/package.ini"
- PKGINI=
- if [ -f "${PKGINIB}" ]; then PKGINI="${PKGINIB}"; fi
- if [ -f "${PKGINIV}" ]; then PKGINI="${PKGINIV}"; fi
-
- # Extension: Check release/branch on github
- PKGGH=$(ini_foreach ini_output_value "${PKGINI}" "repository.github")
- if [ ! -z "${PKGGH}" ]; then
- URL_TAG="https://codeload.github.com/${PKGGH}/tar.gz/refs/tags/${PKG[1]}"
- URL_BRANCH="https://codeload.github.com/${PKGGH}/tar.gz/refs/heads/${PKG[1]}"
- CODE_TAG=$(curl -X HEAD --fail --dump-header - -o /dev/null "${URL_TAG}" 2>/dev/null | head -1 | awk '{print $2}')
- CODE_BRANCH=$(curl -X HEAD --fail --dump-header - -o /dev/null "${URL_BRANCH}" 2>/dev/null | head -1 | awk '{print $2}')
- if [ "${CODE_TAG}" != "200" ] && [ "${CODE_BRANCH}" != "200" ]; then
- echo "No release or branch '${PKG[1]}' found in the github repository ${PKGGH}." >&2
- echo "Check https://github.com/${PKGGH} to see the available releases and branches" >&2
- exit 1
- fi
- fi
-
- # Add the package to the dependencies
- (echo "dependencies.${PKG[0]}=${PKG[1]}" ; echo -e "${OLD_PKG}") | ini_write "package.ini"
- echo "Added to your package.ini: ${PKG[0]}@${PKG[1]}"
-}
-
-cmds[${#cmds[*]}]="a"
-cmds[${#cmds[*]}]="add"
diff --git a/src/command/add/main.c b/src/command/add/main.c
@@ -0,0 +1,420 @@
+#include <dirent.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "cofyc/argparse.h"
+#include "command/command.h"
+#include "common/fs-utils.h"
+#include "common/github-utils.h"
+#include "common/net-utils.h"
+
+#define CACHE_MAX_AGE_SECONDS (7 * 24 * 60 * 60)
+
+static unsigned long hash_string(const char *str) {
+ unsigned long hash = 5381;
+ int c;
+ while ((c = *str++)) {
+ hash = ((hash << 5) + hash) + c;
+ }
+ return hash;
+}
+
+static void url_to_cache_filename(const char *url, char *filename, size_t size) {
+ unsigned long hash = hash_string(url);
+ snprintf(filename, size, "%lu", hash);
+}
+
+static int is_cache_outdated(const char *cache_path) {
+ struct stat st;
+ if (stat(cache_path, &st) != 0) {
+ return 1;
+ }
+ time_t now = time(NULL);
+ time_t file_age = now - st.st_mtime;
+ return file_age > CACHE_MAX_AGE_SECONDS;
+}
+
+static int extract_version_from_depname(const char *depname_with_version, char *depname, char *version,
+ size_t depname_size, size_t version_size) {
+ const char *at_pos = strchr(depname_with_version, '@');
+ if (!at_pos) {
+ strncpy(depname, depname_with_version, depname_size - 1);
+ depname[depname_size - 1] = '\0';
+ version[0] = '\0';
+ return 0;
+ }
+
+ size_t depname_len = at_pos - depname_with_version;
+ if (depname_len >= depname_size) {
+ depname_len = depname_size - 1;
+ }
+ strncpy(depname, depname_with_version, depname_len);
+ depname[depname_len] = '\0';
+
+ strncpy(version, at_pos + 1, version_size - 1);
+ version[version_size - 1] = '\0';
+ return 0;
+}
+
+static int parse_manifest_line(const char *line, char *depname, char *version, char *url, size_t depname_size,
+ size_t version_size, size_t url_size) {
+ char *line_copy = strdup(line);
+ if (!line_copy) return -1;
+
+ char *comment = strchr(line_copy, '#');
+ if (comment) *comment = '\0';
+
+ char *trimmed = trim_whitespace(line_copy);
+ if (strlen(trimmed) == 0) {
+ free(line_copy);
+ return -1;
+ }
+
+ char *space_pos = strchr(trimmed, ' ');
+ char *tab_pos = strchr(trimmed, '\t');
+ char *first_ws = NULL;
+
+ if (space_pos && tab_pos) {
+ first_ws = (space_pos < tab_pos) ? space_pos : tab_pos;
+ } else if (space_pos) {
+ first_ws = space_pos;
+ } else if (tab_pos) {
+ first_ws = tab_pos;
+ }
+
+ int result = 0;
+
+ if (first_ws) {
+ size_t name_len = first_ws - trimmed;
+ if (name_len >= depname_size) {
+ name_len = depname_size - 1;
+ }
+ strncpy(depname, trimmed, name_len);
+ depname[name_len] = '\0';
+
+ char *url_start = trim_whitespace(first_ws + 1);
+ strncpy(url, url_start, url_size - 1);
+ url[url_size - 1] = '\0';
+ } else {
+ strncpy(depname, trimmed, depname_size - 1);
+ depname[depname_size - 1] = '\0';
+ url[0] = '\0';
+ }
+
+ char version_from_depname[256];
+ extract_version_from_depname(depname, depname, version_from_depname, depname_size, sizeof(version_from_depname));
+
+ if (version_from_depname[0] != '\0') {
+ strncpy(version, version_from_depname, version_size - 1);
+ version[version_size - 1] = '\0';
+ } else {
+ version[0] = '\0';
+ }
+
+ free(line_copy);
+ return result;
+}
+
+static int version_matches(const char *requested, const char *available) {
+ if (!requested || requested[0] == '\0') {
+ return 1;
+ }
+ if (!available || available[0] == '\0') {
+ return 0;
+ }
+ return strcmp(requested, available) == 0;
+}
+
+static int append_to_dep_file(const char *name, const char *spec) {
+ const char *dep_path = ".dep";
+ FILE *f = fopen(dep_path, "a");
+ if (!f) {
+ fprintf(stderr, "Error: could not open .dep file for writing\n");
+ return -1;
+ }
+
+ fseek(f, 0, SEEK_END);
+ long pos = ftell(f);
+ if (pos > 0) {
+ fputc('\n', f);
+ }
+
+ if (spec && spec[0] != '\0') {
+ fprintf(f, "%s %s\n", name, spec);
+ } else {
+ fprintf(f, "%s\n", name);
+ }
+
+ fclose(f);
+ return 0;
+}
+
+static int add_from_url(const char *name, const char *url) {
+ int result = append_to_dep_file(name, url);
+ if (result == 0) {
+ printf("Added %s to .dep\n", name);
+ }
+ return result;
+}
+
+static int add_from_repository(const char *name, const char *requested_version) {
+ char *repo_dir = get_repo_dir();
+ if (!repo_dir) {
+ return -1;
+ }
+
+ char *cache_dir = get_cache_dir();
+ if (!cache_dir) {
+ free(repo_dir);
+ return -1;
+ }
+
+ mkdir_recursive(cache_dir);
+
+ DIR *dir = opendir(repo_dir);
+ if (!dir) {
+ fprintf(stderr, "Error: could not open repository directory\n");
+ free(repo_dir);
+ free(cache_dir);
+ return -1;
+ }
+
+ struct dirent *entry;
+ int found = 0;
+
+ while (!found && (entry = readdir(dir)) != NULL) {
+ if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
+
+ char *repo_dir2 = get_repo_dir();
+ if (!repo_dir2) continue;
+
+ char filepath[PATH_MAX];
+ snprintf(filepath, sizeof(filepath), "%s%s", repo_dir2, entry->d_name);
+ free(repo_dir2);
+
+ struct stat st;
+ if (stat(filepath, &st) < 0 || !S_ISREG(st.st_mode)) continue;
+
+ FILE *repo_file = fopen(filepath, "r");
+ if (!repo_file) continue;
+
+ char line[LINE_MAX];
+ while (!found && fgets(line, sizeof(line), repo_file)) {
+ char *comment = strchr(line, '#');
+ if (comment) *comment = '\0';
+
+ char *trimmed = trim_whitespace(line);
+ if (strlen(trimmed) == 0) continue;
+
+ char repo_name[256] = {0};
+ char manifest_url[1024] = {0};
+
+ char *space_pos = strchr(trimmed, ' ');
+ char *tab_pos = strchr(trimmed, '\t');
+ char *first_ws = NULL;
+
+ if (space_pos && tab_pos) {
+ first_ws = (space_pos < tab_pos) ? space_pos : tab_pos;
+ } else if (space_pos) {
+ first_ws = space_pos;
+ } else if (tab_pos) {
+ first_ws = tab_pos;
+ }
+
+ if (!first_ws) continue;
+
+ size_t name_len = first_ws - trimmed;
+ if (name_len >= sizeof(repo_name)) {
+ name_len = sizeof(repo_name) - 1;
+ }
+ strncpy(repo_name, trimmed, name_len);
+ repo_name[name_len] = '\0';
+
+ char *url_start = trim_whitespace(first_ws + 1);
+ strncpy(manifest_url, url_start, sizeof(manifest_url) - 1);
+ manifest_url[sizeof(manifest_url) - 1] = '\0';
+
+ if (strlen(manifest_url) == 0) continue;
+
+ char cache_filename[256];
+ url_to_cache_filename(manifest_url, cache_filename, sizeof(cache_filename));
+
+ char cache_path[PATH_MAX];
+ snprintf(cache_path, sizeof(cache_path), "%s%s", cache_dir, cache_filename);
+
+ if (is_cache_outdated(cache_path)) {
+ printf("Updating cache for repository %s...\n", repo_name);
+ size_t size;
+ char *manifest_content = download_url(manifest_url, &size);
+ if (manifest_content) {
+ FILE *cache_file = fopen(cache_path, "w");
+ if (cache_file) {
+ fwrite(manifest_content, 1, size, cache_file);
+ fclose(cache_file);
+ }
+ free(manifest_content);
+ }
+ }
+
+ FILE *cache_file = fopen(cache_path, "r");
+ if (!cache_file) continue;
+
+ char manifest_line[LINE_MAX];
+ while (!found && fgets(manifest_line, sizeof(manifest_line), cache_file)) {
+ char depname[256] = {0};
+ char version[256] = {0};
+ char tarball_url[1024] = {0};
+
+ if (parse_manifest_line(manifest_line, depname, version, tarball_url, sizeof(depname), sizeof(version),
+ sizeof(tarball_url)) != 0) {
+ continue;
+ }
+
+ if (strcmp(depname, name) != 0) continue;
+
+ if (!version_matches(requested_version, version)) continue;
+
+ if (strlen(tarball_url) == 0) continue;
+
+ printf("Found %s", name);
+ if (version[0] != '\0') {
+ printf(" version %s", version);
+ }
+ printf(" in repository %s\n", repo_name);
+
+ fclose(cache_file);
+ closedir(dir);
+ free(repo_dir);
+ free(cache_dir);
+ return append_to_dep_file(name, tarball_url);
+ }
+
+ fclose(cache_file);
+ }
+
+ fclose(repo_file);
+ }
+
+ closedir(dir);
+ free(repo_dir);
+ free(cache_dir);
+
+ return found ? 0 : -1;
+}
+
+static int add_from_github(const char *name, const char *requested_version) {
+ char *full_ref = NULL;
+
+ if (requested_version && requested_version[0] != '\0') {
+ full_ref = github_matching_ref(name, requested_version);
+ if (!full_ref) {
+ fprintf(stderr, "Error: ref '%s' not found for %s on GitHub\n", requested_version, name);
+ return -1;
+ }
+ free(full_ref);
+ printf("Found %s version %s on GitHub\n", name, requested_version);
+ fflush(stdout);
+ return append_to_dep_file(name, requested_version);
+ } else {
+ char *branch = github_default_branch(name);
+ if (!branch) {
+ fprintf(stderr, "Error: could not determine default branch for %s on GitHub\n", name);
+ return -1;
+ }
+ printf("Found %s on GitHub (default branch: %s)\n", name, branch);
+ fflush(stdout);
+ free(branch);
+ return append_to_dep_file(name, "");
+ }
+}
+
+static int cmd_add(int argc, const char **argv) {
+ struct argparse_option options[] = {
+ OPT_HELP(),
+ OPT_END(),
+ };
+
+ struct argparse argparse;
+ argparse_init(&argparse, options, NULL, 0);
+ argc = argparse_parse(&argparse, argc, argv);
+
+ if (argc < 1) {
+ fprintf(stderr, "Error: add requires <name> [version]\n");
+ fprintf(stderr, "Usage: dep add <name> [version]\n");
+ return 1;
+ }
+
+ const char *name = argv[0];
+ const char *version = (argc > 1) ? argv[1] : NULL;
+
+ if (version && is_url(version)) {
+ printf("Adding %s as direct URL: %s\n", name, version);
+ return add_from_url(name, version);
+ }
+
+ printf("Searching repositories for %s", name);
+ if (version && strlen(version) > 0) {
+ printf(" with version %s", version);
+ }
+ printf("...\n");
+ fflush(stdout);
+
+ int result = add_from_repository(name, version);
+ if (result == 0) {
+ printf("Added %s to .dep\n", name);
+ fflush(stdout);
+ return 0;
+ }
+
+ printf("Not found in repositories, trying GitHub...\n");
+ fflush(stdout);
+ result = add_from_github(name, version);
+ if (result == 0) {
+ printf("Added %s to .dep\n", name);
+ return 0;
+ }
+
+ fprintf(stderr, "Error: package '%s' not found\n", name);
+ return 1;
+}
+
+void __attribute__((constructor)) cmd_add_setup(void) {
+ struct cmd_struct *cmd = calloc(1, sizeof(struct cmd_struct));
+ if (!cmd) {
+ fprintf(stderr, "Failed to allocate memory for add command\n");
+ return;
+ }
+ cmd->next = commands;
+ cmd->fn = cmd_add;
+ static const char *add_names[] = {"add", "a", NULL};
+ cmd->name = add_names;
+ cmd->display = "a(dd)";
+ cmd->description = "Add a new dependency to the project";
+ cmd->help_text =
+ "dep add - Add a new dependency to the project\n"
+ "\n"
+ "Usage:\n"
+ " dep add <name>\n"
+ " dep add <name> <version>\n"
+ " dep add <name> <url>\n"
+ "\n"
+ "Description:\n"
+ " Add a package to the project's .dep file.\n"
+ "\n"
+ " If a version is not specified, the latest version from the repository\n"
+ " will be used, or the default branch from GitHub.\n"
+ "\n"
+ " You can also add a dependency with a direct URL.\n"
+ "\n"
+ "Examples:\n"
+ " dep add finwo/palloc # Latest from repo or GitHub main branch\n"
+ " dep add finwo/palloc edge # Specific version/branch\n"
+ " dep add finwo/palloc v1.0.0 # Specific tag\n"
+ " dep add mylib https://example.com/mylib.tar.gz\n";
+ commands = cmd;
+}
diff --git a/src/command/command.h b/src/command/command.h
@@ -0,0 +1,10 @@
+struct cmd_struct {
+ void *next;
+ const char **name;
+ const char *display;
+ const char *description;
+ const char *help_text;
+ int (*fn)(int, const char **);
+};
+
+extern struct cmd_struct *commands;
diff --git a/src/command/help/index.sh b/src/command/help/index.sh
@@ -1,33 +0,0 @@
-declare -A help_topics
-
-read -r -d '' help_topics[global] <<- EOF
-# #include "topic/global.txt"
-EOF
-
-HELP_TOPIC=global
-function arg_h {
- arg_help "$@"
- return $?
-}
-function arg_help {
- if [[ $# -gt 0 ]]; then
- HELP_TOPIC=$1
- fi
- shift
-}
-
-function cmd_h {
- cmd_help "$@"
- return $?
-}
-function cmd_help {
- if [ -z "${help_topics[$HELP_TOPIC]}" ]; then
- echo "Unknown topic: $HELP_TOPIC" >&2
- exit 1
- fi
-
- echo -e "\n${help_topics[$HELP_TOPIC]}\n"
-}
-
-cmds[${#cmds[*]}]="h"
-cmds[${#cmds[*]}]="help"
diff --git a/src/command/help/main.c b/src/command/help/main.c
@@ -0,0 +1,96 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "command/command.h"
+
+static void print_global_help(void) {
+ printf("Usage: dep [global options] <command> [command options]\n");
+ printf("\n");
+ printf("Global options:\n");
+ printf(" n/a\n");
+ printf("\n");
+ printf("Commands:\n");
+
+ struct cmd_struct *cmd = commands;
+ while (cmd) {
+ printf(" %-16s %s\n", cmd->display ? cmd->display : cmd->name[0], cmd->description ? cmd->description : "");
+ cmd = cmd->next;
+ }
+
+ printf("\n");
+ printf("Help topics:\n");
+ printf(" global This help text\n");
+
+ cmd = commands;
+ while (cmd) {
+ printf(" %-16s More detailed explanation on the %s command\n", cmd->name[0], cmd->name[0]);
+ cmd = cmd->next;
+ }
+}
+
+static int cmd_help(int argc, const char **argv) {
+ if (argc < 1) {
+ print_global_help();
+ return 0;
+ }
+
+ if (argc == 1 && (!strcmp(argv[0], "help") || !strcmp(argv[0], "h") || !strcmp(argv[0], "global"))) {
+ print_global_help();
+ return 0;
+ }
+
+ const char *topic = (argc > 1) ? argv[1] : argv[0];
+
+ if (!strcmp(topic, "global")) {
+ print_global_help();
+ return 0;
+ }
+
+ struct cmd_struct *cmd = commands;
+ while (cmd) {
+ if (!strcmp(topic, cmd->name[0])) {
+ if (cmd->help_text) {
+ printf("%s\n", cmd->help_text);
+ } else {
+ printf("dep %s - %s\n\n", cmd->name[0], cmd->description);
+ printf(" %s\n", cmd->display);
+ }
+ return 0;
+ }
+ cmd = cmd->next;
+ }
+
+ fprintf(stderr, "Error: unknown help topic '%s'\n", topic);
+ fprintf(stderr, "Run 'dep help' for available topics.\n");
+ return 1;
+}
+
+void __attribute__((constructor)) cmd_help_setup(void) {
+ struct cmd_struct *cmd = calloc(1, sizeof(struct cmd_struct));
+ if (!cmd) {
+ fprintf(stderr, "Failed to allocate memory for help command\n");
+ return;
+ }
+ cmd->next = commands;
+ cmd->fn = cmd_help;
+ static const char *help_names[] = {"help", "h", NULL};
+ cmd->name = help_names;
+ cmd->display = "help [topic]";
+ cmd->description = "Show this help or the top-level info about a command";
+ cmd->help_text =
+ "dep help - Show this help or the top-level info about a command\n"
+ "\n"
+ "Usage:\n"
+ " dep help\n"
+ " dep help <command>\n"
+ "\n"
+ "Description:\n"
+ " Show general help or detailed help for a specific command.\n"
+ "\n"
+ "Examples:\n"
+ " dep help # Show general help\n"
+ " dep help add # Show help for add command\n"
+ " dep help install # Show help for install command\n";
+ commands = cmd;
+}
diff --git a/src/command/help/topic/global.txt b/src/command/help/topic/global.txt
@@ -1,16 +0,0 @@
-Usage: __NAME [global options] <command> [options] [-- ...args]
-
-Global options:
- n/a
-
-Commands:
- a(dd) Add a new dependency to the project
- i(nstall) Install all the project's dependencies
- h(elp) [topic] Show this help or the top-level info about a command
- r(epo(sitory)) Repository management
-
-Help topics:
- global This help text
- add More detailed explanation on the add command
- install More detailed explanation on the install command
- repository More detailed explanation on the repository command
diff --git a/src/command/init/main.c b/src/command/init/main.c
@@ -0,0 +1,74 @@
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "command/command.h"
+
+static int cmd_init(int argc, const char **argv) {
+ const char *target_dir = ".";
+ if (argc >= 2) {
+ target_dir = argv[1];
+ }
+
+ // Check if target directory exists and is accessible
+ if (access(target_dir, F_OK | X_OK) != 0) {
+ fprintf(stderr, "Error: directory '%s' does not exist or is not accessible\n", target_dir);
+ return 1;
+ }
+
+ // Build path to .dep file
+ char dep_path[PATH_MAX];
+ int ret = snprintf(dep_path, sizeof(dep_path), "%s/.dep", target_dir);
+ if (ret < 0 || ret >= sizeof(dep_path)) {
+ fprintf(stderr, "Error: path too long\n");
+ return 1;
+ }
+
+ // Check if .dep already exists
+ if (access(dep_path, F_OK) == 0) {
+ printf("Target directory already initialized\n");
+ return 0;
+ }
+
+ // Create empty .dep file
+ FILE *f = fopen(dep_path, "w");
+ if (!f) {
+ fprintf(stderr, "Error: could not create .dep file in '%s'\n", target_dir);
+ return 1;
+ }
+ fclose(f);
+
+ printf("Initialized successfully\n");
+ return 0;
+}
+
+void __attribute__((constructor)) cmd_init_setup(void) {
+ struct cmd_struct *cmd = calloc(1, sizeof(struct cmd_struct));
+ if (!cmd) {
+ fprintf(stderr, "Failed to allocate memory for init command\n");
+ return;
+ }
+ cmd->next = commands;
+ cmd->fn = cmd_init;
+ static const char *init_names[] = {"init", NULL};
+ cmd->name = init_names;
+ cmd->display = "init";
+ cmd->description = "Initialize a new project with a .dep file";
+ cmd->help_text =
+ "dep init - Initialize a new project with a .dep file\n"
+ "\n"
+ "Usage:\n"
+ " dep init\n"
+ " dep init <directory>\n"
+ "\n"
+ "Description:\n"
+ " Create an empty .dep file in the current directory or a specified directory.\n"
+ " The .dep file is used to list dependencies for the project.\n"
+ "\n"
+ "Examples:\n"
+ " dep init # Create .dep in current directory\n"
+ " dep init /path/to/project # Create .dep in specified directory\n";
+ commands = cmd;
+}
diff --git a/src/command/install/help.txt b/src/command/install/help.txt
@@ -1,7 +0,0 @@
-Usage: __NAME [global options] install
-
-Description:
-
- The install command will iterate over all dependencies listed in your
- project's package.ini and install them 1-by-1, installing the dependency's
- dependencies as well.
diff --git a/src/command/install/index.sh b/src/command/install/index.sh
@@ -1,183 +0,0 @@
-# #include "util/ini.sh"
-# #include "util/ostype.sh"
-
-read -r -d '' help_topics[install] <<- EOF
-# #include "help.txt"
-EOF
-
-CMD_INSTALL_PKG_NAME=
-CMD_INSTALL_PKG_DEST="lib"
-
-function arg_i {
- arg_install "$@"
- return $?
-}
-function arg_install {
- return 0
-}
-
-function cmd_i {
- cmd_install "$@"
- return $?
-}
-function cmd_install {
-
- # Sanity check
- PACKAGE_PATH="$(pwd)/package.ini"
- if [ ! -f "${PACKAGE_PATH}" ]; then
- echo "No package.ini in the working directory!" >&2
- exit 1
- fi
-
- # Fetch where to install dependencies
- CMD_INSTALL_PKG_DEST=$(ini_foreach ini_output_value "${PACKAGE_PATH}" "package.deps")
- if [ -z "${CMD_INSTALL_PKG_DEST}" ]; then CMD_INSTALL_PKG_DEST="lib"; fi
-
- # Reset working directory - keep cache
- rm -fr "${CMD_INSTALL_PKG_DEST}/.__NAME/include"
- mkdir -p "${CMD_INSTALL_PKG_DEST}/.__NAME/include"
- echo "INCLUDES+=-I${CMD_INSTALL_PKG_DEST}/.__NAME/include" > "${CMD_INSTALL_PKG_DEST}/.__NAME/config.mk"
- echo -n "" > "${CMD_INSTALL_PKG_DEST}/.__NAME/exported"
-
- # Install all dependencies
- ini_foreach cmd_install_parse_ini "${PACKAGE_PATH}"
- echo "Done"
-}
-
-cmds[${#cmds[*]}]="i"
-cmds[${#cmds[*]}]="install"
-
-declare -A CMD_INSTALL_DEPS
-function cmd_install_parse_ini {
- case "$1" in
- dependencies.)
- cmd_install_dep "$2" "$3"
- ;;
- esac
-
- # package.)
- # case "$2" in
- # name)
- # CMD_INSTALL_PKG_NAME="$3"
- # ;;
- # deps)
- # CMD_INSTALL_PKG_DEST="$3"
- # ;;
- # esac
- # ;;
- # dependencies.)
- # CMD_INSTALL_DEPS["$2"]="$3"
- # ;;
- # esac
-
-}
-
-# function cmd_install_execute {
-# cmd_install_reset_generated
-# for key in "${!CMD_INSTALL_DEPS[@]}"; do
-# cmd_install_dep "$key" "${CMD_INSTALL_DEPS[$key]}"
-# done
-# }
-
-function cmd_install_dep {
- local PKGNAME=$1
- local PKGVER=$2
-
- # Fetch versioned ini
- local PKGINIB="${HOME}/.config/finwo/__NAME/packages/${PKGNAME}/package.ini"
- local PKGINIV="${HOME}/.config/finwo/__NAME/packages/${PKGNAME}/${PKGVER}/package.ini"
- local PKGINI=
- if [ -f "${PKGINIB}" ]; then PKGINI="${PKGINIB}"; fi
- if [ -f "${PKGINIV}" ]; then PKGINI="${PKGINIV}"; fi
- if [ -z "${PKGINI}" ]; then
- echo "No package configuration found for ${PKGNAME}" >&2
- exit 1
- fi
-
- local PKG_DIR="${CMD_INSTALL_PKG_DEST}/${PKGNAME}"
- if [ -d "${PKG_DIR}" ]; then
- # Already installed, just update the pkgini ref
- PKGINI="${CMD_INSTALL_PKG_DEST}/${PKGNAME}/package.ini"
- else
- # Not installed yet, fetch code & run build steps
-
- # Copy repository's config for the package
- local PKG_SRC=$(dirname "${PKGINI}")
- mkdir -p "$(dirname "${PKG_DIR}")"
- cp -r "${PKG_SRC}" "$(dirname ${PKG_DIR})"
- PKGINI="${PKG_DIR}/package.ini"
-
- # Extended fetching detection
- local PKG_GH=$(ini_foreach ini_output_value "${PKGINI}" "repository.github")
- local PKG_TARBALL=$(ini_foreach ini_output_value "${PKGINI}" "package.src")
-
- # Fetch target tarball from github repo
- if [ ! -z "${PKG_GH}" ]; then
- URL_TAG="https://codeload.github.com/${PKG_GH}/tar.gz/refs/tags/${PKGVER}"
- URL_BRANCH="https://codeload.github.com/${PKG_GH}/tar.gz/refs/heads/${PKGVER}"
- CODE_TAG=$(curl -X HEAD --fail --dump-header - -o /dev/null "${URL_TAG}" 2>/dev/null | head -1 | awk '{print $2}')
- CODE_BRANCH=$(curl -X HEAD --fail --dump-header - -o /dev/null "${URL_BRANCH}" 2>/dev/null | head -1 | awk '{print $2}')
- if [ "${CODE_TAG}" == "200" ]; then
- PKG_TARBALL="${URL_TAG}"
- elif [ "${CODE_BRANCH}" == "200" ]; then
- PKG_TARBALL="${URL_BRANCH}"
- fi
- fi
-
- # Fetch configured or detected tarball
- if [ ! -z "${PKG_TARBALL}" ]; then
- # Downloads a tarball and extracts if over the package in our dependency directory
- # TARBALL_FILE="${HOME}/.config/finwo/__NAME/cache/${PKGNAME}/${PKGVER}.tar.gz"
- TARBALL_FILE="${CMD_INSTALL_PKG_DEST}/.__NAME/cache/${PKGNAME}/${PKGVER}.tar.gz"
- mkdir -p "$(dirname "${TARBALL_FILE}")"
- if [ ! -f "${TARBALL_FILE}" ]; then
- curl --location --progress-bar "${PKG_TARBALL}" --output "${TARBALL_FILE}"
- fi
- tar --extract --directory "${PKG_DIR}/" --strip-components 1 --file="${TARBALL_FILE}"
- fi
-
- # Install this dependency's dependencies
- while read line; do
- depname=${line%%=*}
- depver=${line#*=}
- cmd_install_dep "$depname" "$depver"
- done < <(ini_foreach ini_output_section "${PKGINI}" "dependencies." | sort --human-numeric-sort)
-
- # Handle any global build-steps defined in the package.ini
- while read line; do
- buildcmd=${line#*=}
- echo + $buildcmd
- bash -c "cd '${PKG_DIR}' ; ${buildcmd}"
- done < <(ini_foreach ini_output_section "${PKGINI}" "build." | sort --human-numeric-sort)
-
- # Handle any os-generic build-steps defined in the package.ini
- while read line; do
- buildcmd=${line#*=}
- echo + $buildcmd
- bash -c "cd '${PKG_DIR}' ; ${buildcmd}"
- done < <(ini_foreach ini_output_section "${PKGINI}" "build-$(ostype)." | sort --human-numeric-sort)
- fi
-
- # Build the package's exports
- if ! grep "${PKGNAME}" "${CMD_INSTALL_PKG_DEST}/.__NAME/exported" &>/dev/null ; then
- echo "${PKGNAME}" >> "${CMD_INSTALL_PKG_DEST}/.__NAME/exported"
- while read line; do
- filetarget=${line%%=*}
- filesource=${line#*=}
- mkdir -p "$(dirname "${CMD_INSTALL_PKG_DEST}/.__NAME/${filetarget}")"
- case "${filetarget}" in
- exported|cache/*)
- # Blocked
- ;;
- config.mk)
- cat "${PKG_DIR}/${filesource}" | sed "s|__DIRNAME|${PKG_DIR}|g" >> "${CMD_INSTALL_PKG_DEST}/.__NAME/${filetarget}"
- ;;
- *)
- ln -sf "$(pwd)/${PKG_DIR}/${filesource}" "${CMD_INSTALL_PKG_DEST}/.__NAME/${filetarget}"
- # cp "${PKG_DIR}/${filesource}" "${CMD_INSTALL_PKG_DEST}/.__NAME/${filetarget}"
- ;;
- esac
- done < <(ini_foreach ini_output_section "${PKGINI}" "export.")
- fi
-
-}
diff --git a/src/command/install/main.c b/src/command/install/main.c
@@ -0,0 +1,554 @@
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "command/command.h"
+#include "common/github-utils.h"
+#include "common/net-utils.h"
+#include "emmanuel-marty/em_inflate.h"
+#include "erkkah/naett.h"
+#include "rxi/microtar.h"
+
+/* Forward declarations */
+static int install_dependency(const char *name, const char *spec);
+
+static int dir_exists(const char *path) {
+ struct stat st;
+ return stat(path, &st) == 0 && S_ISDIR(st.st_mode);
+}
+
+static int mkdir_recursive(const char *path) {
+ char tmp[PATH_MAX];
+ char *p = NULL;
+ size_t len;
+
+ snprintf(tmp, sizeof(tmp), "%s", path);
+ len = strlen(tmp);
+ if (tmp[len - 1] == '/') {
+ tmp[len - 1] = '\0';
+ }
+
+ for (p = tmp + 1; *p; p++) {
+ if (*p == '/') {
+ *p = '\0';
+ mkdir(tmp, 0755);
+ *p = '/';
+ }
+ }
+ return mkdir(tmp, 0755);
+}
+
+static char *trim_whitespace(char *str) {
+ while (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r') str++;
+ if (*str == '\0') return str;
+ char *end = str + strlen(str) - 1;
+ while (end > str && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) {
+ *end = '\0';
+ end--;
+ }
+ return str;
+}
+
+static char *spec_to_url(const char *name, const char *spec) {
+ if (strlen(spec) > 0 && is_url(spec)) {
+ return strdup(spec);
+ }
+
+ char *full_ref = NULL;
+
+ if (strlen(spec) > 0) {
+ full_ref = github_matching_ref(name, spec);
+ if (!full_ref) {
+ fprintf(stderr, "Error: ref '%s' not found for %s\n", spec, name);
+ return NULL;
+ }
+ } else {
+ char *branch = github_default_branch(name);
+ if (!branch) {
+ fprintf(stderr, "Warning: could not determine default branch for %s, using 'main'\n", name);
+ branch = strdup("main");
+ }
+ full_ref = malloc(256);
+ if (full_ref) {
+ snprintf(full_ref, 256, "refs/heads/%s", branch);
+ }
+ free(branch);
+ }
+
+ if (!full_ref) {
+ fprintf(stderr, "Error: could not determine ref for %s\n", name);
+ return NULL;
+ }
+
+ char *url = malloc(2048);
+ if (!url) {
+ free(full_ref);
+ return NULL;
+ }
+ snprintf(url, 2048, "https://github.com/%s/archive/%s.tar.gz", name, full_ref);
+ free(full_ref);
+ return url;
+}
+
+static int process_dep_export_file(const char *dep_dir, const char *name) {
+ char export_path[PATH_MAX];
+ snprintf(export_path, sizeof(export_path), "%s/.dep.export", dep_dir);
+ FILE *f = fopen(export_path, "r");
+ if (!f) {
+ return 0;
+ }
+
+ char dep_base[PATH_MAX];
+ if (getcwd(dep_base, sizeof(dep_base)) == NULL) {
+ fprintf(stderr, "Error: failed to get current working directory\n");
+ fclose(f);
+ return -1;
+ }
+
+ char line[PATH_MAX];
+ while (fgets(line, sizeof(line), f)) {
+ char *comment = strchr(line, '#');
+ if (comment) *comment = '\0';
+
+ char *trimmed = trim_whitespace(line);
+ if (strlen(trimmed) == 0) continue;
+
+ char source[512] = {0};
+ char target[512] = {0};
+
+ char *space_pos = strchr(trimmed, ' ');
+ char *tab_pos = strchr(trimmed, '\t');
+ char *first_whitespace = NULL;
+ if (space_pos && tab_pos) {
+ first_whitespace = (space_pos < tab_pos) ? space_pos : tab_pos;
+ } else if (space_pos) {
+ first_whitespace = space_pos;
+ } else if (tab_pos) {
+ first_whitespace = tab_pos;
+ }
+
+ if (first_whitespace) {
+ size_t source_len = first_whitespace - trimmed;
+ strncpy(source, trimmed, source_len);
+ source[source_len] = '\0';
+ strcpy(target, trim_whitespace(first_whitespace + 1));
+ } else {
+ continue;
+ }
+
+ if (strlen(source) == 0 || strlen(target) == 0) continue;
+
+ char source_parent[PATH_MAX];
+ strncpy(source_parent, source, sizeof(source_parent) - 1);
+ source_parent[sizeof(source_parent) - 1] = '\0';
+ char *last_slash = strrchr(source_parent, '/');
+ if (last_slash) {
+ *last_slash = '\0';
+ char parent_dir[PATH_MAX];
+ snprintf(parent_dir, sizeof(parent_dir), "lib/.dep/%s", source_parent);
+ mkdir_recursive(parent_dir);
+ } else {
+ mkdir_recursive("lib/.dep");
+ }
+
+ char target_abs[PATH_MAX];
+ snprintf(target_abs, sizeof(target_abs), "%s/lib/%s/%s", dep_base, name, target);
+
+ char link_path[PATH_MAX];
+ snprintf(link_path, sizeof(link_path), "lib/.dep/%s", source);
+
+ unlink(link_path);
+ if (symlink(target_abs, link_path) != 0) {
+ fprintf(stderr, "Warning: failed to create symlink %s -> %s\n", link_path, target_abs);
+ } else {
+ printf("Exported %s -> %s\n", source, target_abs);
+ }
+ }
+
+ fclose(f);
+ return 0;
+}
+
+static int process_dep_file_in_dir(const char *dep_dir);
+static int execute_postinstall_hook(const char *dep_dir);
+
+static int process_dep_file_in_dir(const char *dep_dir) {
+ char dep_path[PATH_MAX];
+ snprintf(dep_path, sizeof(dep_path), "%s/.dep", dep_dir);
+ FILE *f = fopen(dep_path, "r");
+ if (!f) {
+ // No .dep file is not an error
+ return 0;
+ }
+
+ char line[LINE_MAX];
+ while (fgets(line, sizeof(line), f)) {
+ char *comment = strchr(line, '#');
+ if (comment) *comment = '\0';
+
+ char *trimmed = trim_whitespace(line);
+ if (strlen(trimmed) == 0) continue;
+
+ char name[256] = {0};
+ char spec[1024] = {0};
+
+ char *space_pos = strchr(trimmed, ' ');
+ char *tab_pos = strchr(trimmed, '\t');
+ char *first_whitespace = NULL;
+ if (space_pos && tab_pos) {
+ first_whitespace = (space_pos < tab_pos) ? space_pos : tab_pos;
+ } else if (space_pos) {
+ first_whitespace = space_pos;
+ } else if (tab_pos) {
+ first_whitespace = tab_pos;
+ }
+
+ if (first_whitespace) {
+ size_t name_len = first_whitespace - trimmed;
+ strncpy(name, trimmed, name_len);
+ name[name_len] = '\0';
+ strcpy(spec, trim_whitespace(first_whitespace + 1));
+ } else {
+ strncpy(name, trimmed, sizeof(name) - 1);
+ name[sizeof(name) - 1] = '\0';
+ }
+
+ if (strlen(name) == 0) continue;
+
+ install_dependency(name, spec);
+ }
+
+ fclose(f);
+ return 0;
+}
+
+static int execute_postinstall_hook(const char *dep_dir) {
+ char cwd[PATH_MAX];
+ if (getcwd(cwd, sizeof(cwd)) == NULL) {
+ fprintf(stderr, "Error: failed to get current working directory\n");
+ return -1;
+ }
+
+ if (chdir(dep_dir) != 0) {
+ fprintf(stderr, "Error: failed to change to dependency directory\n");
+ return -1;
+ }
+
+ char hook_path[PATH_MAX];
+ snprintf(hook_path, sizeof(hook_path), "./.dep.hook.postinstall");
+
+ struct stat st;
+ if (stat(hook_path, &st) != 0) {
+ chdir(cwd);
+ return 0;
+ }
+
+ int exec_bits = S_IXUSR | S_IXGRP | S_IXOTH;
+ if (!(st.st_mode & exec_bits)) {
+ fprintf(stderr, "Warning: %s is not executable, making executable...\n", hook_path);
+ if (chmod(hook_path, st.st_mode | exec_bits) != 0) {
+ fprintf(stderr, "Error: failed to make hook executable\n");
+ chdir(cwd);
+ return -1;
+ }
+ if (stat(hook_path, &st) != 0) {
+ chdir(cwd);
+ return 0;
+ }
+ }
+
+ pid_t pid = fork();
+ if (pid == 0) {
+ char *const argv[] = {hook_path, NULL};
+ execve(hook_path, argv, environ);
+ fprintf(stderr, "Error: execve failed: errno=%d\n", errno);
+ _exit(127);
+ } else if (pid < 0) {
+ chdir(cwd);
+ fprintf(stderr, "Error: failed to fork for postinstall hook\n");
+ return -1;
+ }
+
+ int status;
+ waitpid(pid, &status, 0);
+
+ chdir(cwd);
+
+ if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
+ printf("Executed postinstall hook for %s\n", dep_dir);
+ return 0;
+ } else {
+ fprintf(stderr, "Error: postinstall hook failed with exit code %d\n", WIFEXITED(status) ? WEXITSTATUS(status) : -1);
+ return -1;
+ }
+}
+
+static int install_dependency(const char *name, const char *spec) {
+ char lib_path[PATH_MAX];
+ snprintf(lib_path, sizeof(lib_path), "lib/%s", name);
+
+ if (dir_exists(lib_path)) {
+ printf("Skipping %s (already installed)\n", name);
+ return 0;
+ }
+
+ char *url = spec_to_url(name, spec);
+ if (!url) {
+ fprintf(stderr, "Error: failed to resolve spec for %s\n", name);
+ return -1;
+ }
+
+ printf("Installing %s from %s\n", name, url);
+
+ mkdir_recursive(lib_path);
+
+ if (download_and_extract(url, lib_path) != 0) {
+ fprintf(stderr, "Error: failed to install %s\n", name);
+ free(url);
+ return -1;
+ }
+ free(url);
+
+ // Process .dep.chain recursively
+ char dep_chain_path[PATH_MAX];
+ while (1) {
+ // Build .dep.chain path
+ snprintf(dep_chain_path, sizeof(dep_chain_path), "%s/.dep.chain", lib_path);
+
+ // Check if .dep.chain exists
+ FILE *chain_file = fopen(dep_chain_path, "r");
+ if (!chain_file) {
+ break; // No more chaining
+ }
+
+ // Read spec (single line)
+ char spec[1024] = {0};
+ if (!fgets(spec, sizeof(spec), chain_file)) {
+ fclose(chain_file);
+ fprintf(stderr, "Error: failed to read .dep.chain\n");
+ return -1;
+ }
+ fclose(chain_file);
+
+ // Delete .dep.chain file
+ if (remove(dep_chain_path) != 0) {
+ fprintf(stderr, "Warning: failed to remove .dep.chain\n");
+ // Continue anyway - spec was read
+ }
+
+ // Trim whitespace/newline from spec
+ char *trimmed = trim_whitespace(spec);
+ if (strlen(trimmed) == 0) {
+ fprintf(stderr, "Warning: empty spec in .dep.chain\n");
+ continue;
+ }
+
+ printf("Found .dep.chain, chaining to: %s\n", trimmed);
+
+ // Resolve spec to URL
+ char *overlay_url = spec_to_url(name, trimmed);
+ if (!overlay_url) {
+ fprintf(stderr, "Error: failed to resolve chained spec '%s'\n", trimmed);
+ return -1;
+ }
+
+ // Overlay extract (directly over existing files)
+ printf("Overlaying %s from %s\n", name, overlay_url);
+ if (download_and_extract(overlay_url, lib_path) != 0) {
+ fprintf(stderr, "Error: failed to overlay chained dependency\n");
+ free(overlay_url);
+ return -1;
+ }
+ free(overlay_url);
+ // Loop continues to check for new .dep.chain
+ }
+
+ // Process .dep file in the dependency's directory
+ if (process_dep_file_in_dir(lib_path) != 0) {
+ fprintf(stderr, "Warning: failed to process .dep file for %s\n", name);
+ // Not returning error because the dependency itself installed successfully.
+ }
+
+ // Execute postinstall hook if present
+ if (execute_postinstall_hook(lib_path) != 0) {
+ fprintf(stderr, "Error: postinstall hook failed for %s\n", name);
+ return -1;
+ }
+
+ // Handle config.mk: append dependency's config.mk to lib/.dep/config.mk
+ char dep_dir[PATH_MAX];
+ snprintf(dep_dir, sizeof(dep_dir), "lib/.dep");
+ mkdir_recursive(dep_dir);
+
+ char src_config_path[PATH_MAX];
+ snprintf(src_config_path, sizeof(src_config_path), "%s/config.mk", lib_path);
+
+ char dst_config_path[PATH_MAX];
+ snprintf(dst_config_path, sizeof(dst_config_path), "lib/.dep/config.mk");
+
+ FILE *dep_config = fopen(src_config_path, "r");
+ if (dep_config) {
+ FILE *dst_config = fopen(dst_config_path, "a");
+ if (dst_config) {
+ char line[LINE_MAX];
+ while (fgets(line, sizeof(line), dep_config)) {
+ // Replace __DIRNAME and {__DIRNAME__} with the dependency's path (lib_path)
+ char modified[LINE_MAX * 2]; // enough space for replacements
+ char *src = line;
+ char *dst = modified;
+ while (*src) {
+ if (strncmp(src, "__DIRNAME", 9) == 0) {
+ strcpy(dst, lib_path);
+ dst += strlen(lib_path);
+ src += 9;
+ } else if (strncmp(src, "{{module.dirname}}", 18) == 0) {
+ strcpy(dst, lib_path);
+ dst += strlen(lib_path);
+ src += 18;
+ } else {
+ *dst++ = *src++;
+ }
+ }
+ *dst = '\0';
+ fputs(modified, dst_config);
+ }
+ // Ensure a newline at end of appended content if not already ending with newline
+ // (optional, but we can add a newline to separate entries)
+ fputc('\n', dst_config);
+ fclose(dst_config);
+ } else {
+ fprintf(stderr, "Warning: could not open %s for appending\n", dst_config_path);
+ }
+ fclose(dep_config);
+ }
+
+ if (process_dep_export_file(lib_path, name) != 0) {
+ fprintf(stderr, "Warning: failed to process .dep.export file for %s\n", name);
+ }
+
+ printf("Installed %s\n", name);
+ return 0;
+}
+
+static int cmd_install(int argc, const char **argv) {
+ (void)argc;
+ (void)argv;
+
+ const char *dep_path = ".dep";
+ FILE *f = fopen(dep_path, "r");
+ if (!f) {
+ fprintf(stderr, "Error: .dep file not found. Run 'dep init' first.\n");
+ return 1;
+ }
+
+ if (!dir_exists("lib")) {
+ if (mkdir("lib", 0755) != 0) {
+ fprintf(stderr, "Error: could not create lib directory\n");
+ fclose(f);
+ return 1;
+ }
+ }
+
+ char dep_dir[PATH_MAX];
+ snprintf(dep_dir, sizeof(dep_dir), "lib/.dep");
+ mkdir_recursive(dep_dir);
+
+ char config_mk_path[PATH_MAX];
+ snprintf(config_mk_path, sizeof(config_mk_path), "%s/config.mk", dep_dir);
+ FILE *config_mk = fopen(config_mk_path, "w");
+ if (config_mk) {
+ fputs("CFLAGS+=-Ilib/.dep/include\n", config_mk);
+ fclose(config_mk);
+ } else {
+ fprintf(stderr, "Warning: could not create %s\n", config_mk_path);
+ }
+
+ char line[LINE_MAX];
+ int has_deps = 0;
+
+ while (fgets(line, sizeof(line), f)) {
+ char *comment = strchr(line, '#');
+ if (comment) *comment = '\0';
+
+ char *trimmed = trim_whitespace(line);
+ if (strlen(trimmed) == 0) continue;
+
+ has_deps = 1;
+
+ char name[256] = {0};
+ char spec[1024] = {0};
+
+ char *space_pos = strchr(trimmed, ' ');
+ char *tab_pos = strchr(trimmed, '\t');
+ char *first_whitespace = NULL;
+ if (space_pos && tab_pos) {
+ first_whitespace = (space_pos < tab_pos) ? space_pos : tab_pos;
+ } else if (space_pos) {
+ first_whitespace = space_pos;
+ } else if (tab_pos) {
+ first_whitespace = tab_pos;
+ }
+
+ if (first_whitespace) {
+ size_t name_len = first_whitespace - trimmed;
+ strncpy(name, trimmed, name_len);
+ name[name_len] = '\0';
+ strcpy(spec, trim_whitespace(first_whitespace + 1));
+ } else {
+ strncpy(name, trimmed, sizeof(name) - 1);
+ name[sizeof(name) - 1] = '\0';
+ }
+
+ if (strlen(name) == 0) continue;
+
+ install_dependency(name, spec);
+ }
+
+ fclose(f);
+
+ if (!has_deps) {
+ printf("No dependencies to install\n");
+ }
+
+ return 0;
+}
+
+void __attribute__((constructor)) cmd_install_setup(void) {
+ struct cmd_struct *cmd = calloc(1, sizeof(struct cmd_struct));
+ if (!cmd) {
+ fprintf(stderr, "Failed to allocate memory for install command\n");
+ return;
+ }
+ cmd->next = commands;
+ cmd->fn = cmd_install;
+ static const char *install_names[] = {"install", "i", NULL};
+ cmd->name = install_names;
+ cmd->display = "i(nstall)";
+ cmd->description = "Install all the project's dependencies";
+ cmd->help_text =
+ "dep install - Install all the project's dependencies\n"
+ "\n"
+ "Usage:\n"
+ " dep install\n"
+ "\n"
+ "Description:\n"
+ " Install all dependencies listed in the .dep file in the current directory.\n"
+ "\n"
+ " Dependencies are installed to the lib/ directory by default.\n"
+ "\n"
+ " Each dependency is downloaded and extracted to lib/<owner>/<name>/.\n"
+ "\n"
+ " If a dependency itself has dependencies listed in its own .dep file,\n"
+ " those will be installed recursively.\n";
+ commands = cmd;
+}
diff --git a/src/command/license/license.h b/src/command/license/license.h
@@ -0,0 +1,7 @@
+#ifndef LICENSE_H
+#define LICENSE_H
+
+extern const unsigned char _binary_LICENSE_md_start[];
+extern const unsigned char _binary_LICENSE_md_end[];
+
+#endif // LICENSE_H
diff --git a/src/command/license/main.c b/src/command/license/main.c
@@ -0,0 +1,34 @@
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "command/command.h"
+#include "license.h"
+
+int cmd_license(int argc, const char **argv) {
+ (void)argc;
+ (void)argv;
+ const unsigned char *start = _binary_LICENSE_md_start;
+ const unsigned char *end = _binary_LICENSE_md_end;
+ size_t len = end - start;
+ fwrite(start, 1, len, stdout);
+ return 0;
+}
+
+void __attribute__((constructor)) cmd_license_setup() {
+ struct cmd_struct *cmd = calloc(1, sizeof(struct cmd_struct));
+ cmd->next = commands;
+ cmd->fn = cmd_license;
+ static const char *license_names[] = {"license", NULL};
+ cmd->name = license_names;
+ cmd->display = "license";
+ cmd->description = "Show license information";
+ cmd->help_text =
+ "dep license - Show license information\n"
+ "\n"
+ "Usage:\n"
+ " dep license\n"
+ "\n"
+ "Description:\n"
+ " Display the license information for dep.\n";
+ commands = cmd;
+}
diff --git a/src/command/license/main.h b/src/command/license/main.h
diff --git a/src/command/repo/help.txt b/src/command/repo/help.txt
@@ -1,8 +0,0 @@
-Usage: __NAME [global options] repository <command> <argument>
-
-Commands:
-
- a(dd) <name> <manifest-url> Add a repository to fetch packages from
- d(el(ete)) <name> Delete a repository
- c(lean) Remove packages cache
- u(pdate) Update packages cache
diff --git a/src/command/repo/index.sh b/src/command/repo/index.sh
@@ -1,102 +0,0 @@
-# #include "util/ini.sh"
-
-read -r -d '' help_topics[repository] <<- EOF
-# #include "help.txt"
-EOF
-
-CMD_REPO_CMD=
-CMD_REPO_NAME=
-CMD_REPO_LOC=
-
-function arg_r {
- arg_repository "$@"
- return $?
-}
-function arg_repo {
- arg_repository "$@"
- return $?
-}
-function arg_repository {
- CMD_REPO_CMD=$1
-
- case "${CMD_REPO_CMD}" in
- a|add)
- CMD_REPO_NAME=$2
- CMD_REPO_LOC=$3
- ;;
- d|del|delete)
- CMD_REPO_NAME=$2
- ;;
- c|clean)
- # Intentionally empty
- ;;
- u|update)
- # Intentionally empty
- ;;
- *)
- echo "Unknown command: ${CMD_REPO_CMD}" >&2
- exit 1
- ;;
- esac
-
- return 0
-}
-
-function cmd_r {
- cmd_repository "$@"
- return $?
-}
-function cmd_repo {
- cmd_repository "$@"
- return $?
-}
-function cmd_repository {
- case "${CMD_REPO_CMD}" in
- a|add)
- mkdir -p "${HOME}/.config/finwo/__NAME/repositories.d"
- echo "${CMD_REPO_NAME}=${CMD_REPO_LOC}" >> "${HOME}/.config/finwo/__NAME/repositories.d/50-${CMD_REPO_NAME}"
- ;;
- d|del|delete)
- mkdir -p "${HOME}/.config/finwo/__NAME/repositories.d"
- rm -f "${HOME}/.config/finwo/__NAME/repositories.d/*-${CMD_REPO_NAME}"
- ;;
- c|clean)
- rm -rf "${HOME}/.config/finwo/__NAME/packages"
- mkdir -p "${HOME}/.config/finwo/__NAME/packages"
- ;;
- u|update)
- rm -rf "${HOME}/.config/finwo/__NAME/packages"
- mkdir -p "${HOME}/.config/finwo/__NAME/packages"
- mkdir -p "${HOME}/.config/finwo/__NAME/repositories.d"
-
- # Build complete repositories ini
- echo "" > "${HOME}/.config/finwo/__NAME/repositories.tmp"
- if [ -f "${HOME}/.config/finwo/__NAME/repositories" ]; then
- echo "[repository]" >> "${HOME}/.config/finwo/__NAME/repositories.tmp"
- cat "${HOME}/.config/finwo/__NAME/repositories" >> "${HOME}/.config/finwo/__NAME/repositories.tmp"
- fi
- for fname in $(ls "${HOME}/.config/finwo/__NAME/repositories.d/" | sort); do
- echo "[repository]" >> "${HOME}/.config/finwo/__NAME/repositories.tmp"
- cat "${HOME}/.config/finwo/__NAME/repositories.d/${fname}" >> "${HOME}/.config/finwo/__NAME/repositories.tmp"
- done
-
- # Download and extract them
- while read source; do
- curl --location --progress-bar "${source}" | \
- tar --gunzip --extract --directory "${HOME}/.config/finwo/__NAME/packages" --strip-components 1
- done < <(ini_foreach ini_output_value "${HOME}/.config/finwo/__NAME/repositories.tmp" "repository.")
-
- # Aannddd.. we're done with the tmp file
- rm -f "${HOME}/.config/finwo/__NAME/repositories.tmp"
-
- ;;
- *)
- echo "Unknown command: ${CMD_REPO_CMD}" >&2
- exit 1
- ;;
- esac
-}
-
-cmds[${#cmds[*]}]="r"
-cmds[${#cmds[*]}]="repo"
-cmds[${#cmds[*]}]="repository"
diff --git a/src/command/repository/main.c b/src/command/repository/main.c
@@ -0,0 +1,339 @@
+#include <dirent.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "../command.h"
+#include "cofyc/argparse.h"
+#include "common/fs-utils.h"
+
+static int cmd_repository_list(int argc, const char **argv);
+static int cmd_repository_add(int argc, const char **argv);
+static int cmd_repository_remove(int argc, const char **argv);
+static int cmd_repository_clean_cache(int argc, const char **argv);
+
+static const char *const usages[] = {
+ "repository <subcommand> [options]",
+ NULL,
+};
+
+static int cmd_repository(int argc, const char **argv) {
+ struct argparse_option options[] = {
+ OPT_HELP(),
+ OPT_END(),
+ };
+ struct argparse argparse;
+ argparse_init(&argparse, options, usages, 0);
+ argc = argparse_parse(&argparse, argc, argv);
+
+ // If no subcommand provided, show available subcommands
+ if (argc < 1) {
+ printf("Available subcommands:\n");
+ printf(" list List the names of the repositories\n");
+ printf(" add Add a repository: add <name> <url>\n");
+ printf(" remove Remove a repository: remove <name>\n");
+ printf(" clean-cache Remove all cached manifest files\n");
+ return 0;
+ }
+
+ // Dispatch to the appropriate subcommand handler
+ if (!strcmp(argv[0], "list")) {
+ return cmd_repository_list(argc - 1, argv + 1);
+ } else if (!strcmp(argv[0], "add")) {
+ return cmd_repository_add(argc - 1, argv + 1);
+ } else if (!strcmp(argv[0], "remove")) {
+ return cmd_repository_remove(argc - 1, argv + 1);
+ } else if (!strcmp(argv[0], "clean-cache")) {
+ return cmd_repository_clean_cache(argc - 1, argv + 1);
+ } else {
+ fprintf(stderr, "Error: unknown subcommand '%s'\n", argv[0]);
+ return 1;
+ }
+}
+
+// List all repository names from files in the repository directory
+// Files are parsed line by line, with '#' starting comments
+static int cmd_repository_list(int argc, const char **argv) {
+ (void)argc;
+ (void)argv;
+
+ char *repo_dir = get_repo_dir();
+ if (!repo_dir) {
+ return 1;
+ }
+ DIR *dir = opendir(repo_dir);
+ free(repo_dir);
+ if (!dir) {
+ fprintf(stderr, "Error: could not open repository directory\n");
+ return 1;
+ }
+
+ struct dirent *entry;
+ // Iterate through all files in the repository directory
+ while ((entry = readdir(dir)) != NULL) {
+ // Skip . and .. entries
+ if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
+
+ struct stat st;
+ char *repo_dir2 = get_repo_dir();
+ if (!repo_dir2) {
+ closedir(dir);
+ return 1;
+ }
+ char filepath[PATH_MAX];
+ snprintf(filepath, sizeof(filepath), "%s%s", repo_dir2, entry->d_name);
+ free(repo_dir2);
+ // Skip non-regular files
+ if (stat(filepath, &st) < 0 || !S_ISREG(st.st_mode)) continue;
+
+ FILE *f = fopen(filepath, "r");
+ if (!f) continue;
+
+ // Parse each line in the file
+ char line[LINE_MAX];
+ while (fgets(line, sizeof(line), f)) {
+ // Remove trailing newline
+ line[strcspn(line, "\n")] = '\0';
+ // Strip comments (everything after '#')
+ char *comment = strchr(line, '#');
+ if (comment) *comment = '\0';
+ // Extract the first word as the repository name
+ char *name = strtok(line, " \t");
+ if (name && name[0] != '\0') {
+ printf("%s\n", name);
+ }
+ }
+ fclose(f);
+ }
+
+ closedir(dir);
+ return 0;
+}
+
+// Add a repository entry to the 00-managed file
+// Format: <name> <url>
+static int cmd_repository_add(int argc, const char **argv) {
+ if (argc < 2) {
+ fprintf(stderr, "Error: add requires <name> and <url>\n");
+ fprintf(stderr, "Usage: repository add <name> <url>\n");
+ return 1;
+ }
+
+ const char *name = argv[0];
+ const char *url = argv[1];
+
+ // Build the file path: ~/.config/finwo/dep/repositories.d/00-managed
+ char *repo_dir = get_repo_dir();
+ if (!repo_dir) {
+ return 1;
+ }
+ char filepath[PATH_MAX];
+ snprintf(filepath, sizeof(filepath), "%s00-managed", repo_dir);
+ free(repo_dir);
+
+ // Open the file in append mode
+ FILE *f = fopen(filepath, "a");
+ if (!f) {
+ fprintf(stderr, "Error: could not open file '%s' for writing\n", filepath);
+ return 1;
+ }
+
+ // Write the repository entry: name url
+ fprintf(f, "%s %s\n", name, url);
+ fclose(f);
+
+ printf("Repository '%s' added.\n", name);
+ return 0;
+}
+
+// Remove a repository entry by name from all files in the repository directory
+// If a file becomes empty after removal, it is deleted
+static int cmd_repository_remove(int argc, const char **argv) {
+ if (argc < 1) {
+ fprintf(stderr, "Error: remove requires <name>\n");
+ fprintf(stderr, "Usage: repository remove <name>\n");
+ return 1;
+ }
+
+ const char *name = argv[0];
+ int found = 0;
+
+ char *repo_dir = get_repo_dir();
+ if (!repo_dir) {
+ return 1;
+ }
+ DIR *dir = opendir(repo_dir);
+ free(repo_dir);
+ if (!dir) {
+ fprintf(stderr, "Error: could not open repository directory\n");
+ return 1;
+ }
+
+ struct dirent *entry;
+ // Iterate through all files in the repository directory
+ while ((entry = readdir(dir)) != NULL) {
+ // Skip . and .. entries
+ if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
+
+ char *repo_dir2 = get_repo_dir();
+ if (!repo_dir2) {
+ closedir(dir);
+ return 1;
+ }
+ char filepath[PATH_MAX];
+ snprintf(filepath, sizeof(filepath), "%s%s", repo_dir2, entry->d_name);
+ free(repo_dir2);
+
+ struct stat st;
+ // Skip non-regular files
+ if (stat(filepath, &st) < 0 || !S_ISREG(st.st_mode)) continue;
+
+ // Create a temporary file for writing the modified content
+ char temp_path[PATH_MAX];
+ snprintf(temp_path, sizeof(temp_path), "%s.tmp", filepath);
+
+ FILE *in = fopen(filepath, "r");
+ FILE *out = fopen(temp_path, "w");
+ if (!in || !out) {
+ fprintf(stderr, "Error: could not open files for processing '%s'\n", filepath);
+ if (in) fclose(in);
+ if (out) fclose(out);
+ closedir(dir);
+ return 1;
+ }
+
+ // Process each line, removing lines that match the repository name
+ char line[LINE_MAX];
+ while (fgets(line, sizeof(line), in)) {
+ // Make a copy for parsing (preserving original line for output)
+ char line_copy[LINE_MAX];
+ strncpy(line_copy, line, sizeof(line_copy) - 1);
+ line_copy[sizeof(line_copy) - 1] = '\0';
+
+ // Strip comments before matching
+ line_copy[strcspn(line_copy, "\n")] = '\0';
+ char *comment = strchr(line_copy, '#');
+ if (comment) *comment = '\0';
+ char *line_name = strtok(line_copy, " \t");
+
+ // Skip lines that match the repository name
+ if (line_name && !strcmp(line_name, name)) {
+ found = 1;
+ continue;
+ }
+ // Write the original line back to the temp file
+ fputs(line, out);
+ }
+
+ fclose(in);
+ fclose(out);
+
+ // Replace original file with the temporary file
+ if (rename(temp_path, filepath) < 0) {
+ fprintf(stderr, "Error: could not replace file '%s'\n", filepath);
+ closedir(dir);
+ return 1;
+ }
+
+ // If the file is now empty, remove it
+ if (stat(filepath, &st) == 0 && st.st_size == 0) {
+ if (remove(filepath) < 0) {
+ fprintf(stderr, "Error: could not remove empty file '%s'\n", filepath);
+ closedir(dir);
+ return 1;
+ }
+ }
+ }
+
+ closedir(dir);
+
+ if (!found) {
+ fprintf(stderr, "Warning: repository '%s' not found\n", name);
+ } else {
+ printf("Repository '%s' removed.\n", name);
+ }
+
+ return 0;
+}
+
+static int cmd_repository_clean_cache(int argc, const char **argv) {
+ (void)argc;
+ (void)argv;
+
+ char *cache_dir = get_cache_dir();
+ if (!cache_dir) {
+ return 1;
+ }
+
+ DIR *dir = opendir(cache_dir);
+ if (!dir) {
+ free(cache_dir);
+ return 0;
+ }
+
+ struct dirent *entry;
+ int removed = 0;
+
+ while ((entry = readdir(dir)) != NULL) {
+ if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
+
+ char filepath[PATH_MAX];
+ snprintf(filepath, sizeof(filepath), "%s%s", cache_dir, entry->d_name);
+
+ struct stat st;
+ if (stat(filepath, &st) < 0 || !S_ISREG(st.st_mode)) continue;
+
+ if (remove(filepath) == 0) {
+ removed++;
+ }
+ }
+
+ closedir(dir);
+ free(cache_dir);
+
+ printf("Removed %d cached manifest file%s.\n", removed, removed == 1 ? "" : "s");
+ return 0;
+}
+
+// Register the repository command with the command system
+void __attribute__((constructor)) cmd_repository_setup(void) {
+ struct cmd_struct *cmd = calloc(1, sizeof(struct cmd_struct));
+ if (!cmd) {
+ fprintf(stderr, "Failed to allocate memory for repository command\n");
+ return;
+ }
+ cmd->next = commands;
+ cmd->fn = cmd_repository;
+ static const char *repository_names[] = {"repository", "repo", "r", NULL};
+ cmd->name = repository_names;
+ cmd->display = "r(epo(sitory))";
+ cmd->description = "Repository management";
+ cmd->help_text =
+ "dep repository - Repository management\n"
+ "\n"
+ "Usage:\n"
+ " dep repository list\n"
+ " dep repository add <name> <url>\n"
+ " dep repository remove <name>\n"
+ " dep repository clean-cache\n"
+ "\n"
+ "Description:\n"
+ " dep can use custom repositories to discover packages. Repositories are\n"
+ " configured in ~/.config/finwo/dep/repositories.d/.\n"
+ "\n"
+ "Subcommands:\n"
+ " list List the names of the configured repositories\n"
+ " add Add a repository: dep repository add <name> <url>\n"
+ " remove Remove a repository: dep repository remove <name>\n"
+ " clean-cache Remove all cached manifest files\n"
+ "\n"
+ "Examples:\n"
+ " dep repository add myorg https://example.com/path/to/manifest\n"
+ " dep repository list\n"
+ " dep repository remove myorg\n"
+ " dep repository clean-cache\n";
+ commands = cmd;
+}
diff --git a/src/common/fs-utils.c b/src/common/fs-utils.c
@@ -0,0 +1,72 @@
+#include "fs-utils.h"
+
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+char *get_repo_dir(void) {
+ const char *home = getenv("HOME");
+ if (!home) {
+ fprintf(stderr, "Error: HOME environment variable not set\n");
+ return NULL;
+ }
+ size_t len = strlen(home) + strlen(REPO_DIR_DEFAULT) + 1;
+ char *path = malloc(len);
+ if (!path) {
+ fprintf(stderr, "Error: out of memory\n");
+ return NULL;
+ }
+ snprintf(path, len, "%s%s", home, REPO_DIR_DEFAULT);
+ return path;
+}
+
+char *get_cache_dir(void) {
+ const char *home = getenv("HOME");
+ if (!home) {
+ fprintf(stderr, "Error: HOME environment variable not set\n");
+ return NULL;
+ }
+ size_t len = strlen(home) + strlen(CACHE_DIR_DEFAULT) + 1;
+ char *path = malloc(len);
+ if (!path) {
+ fprintf(stderr, "Error: out of memory\n");
+ return NULL;
+ }
+ snprintf(path, len, "%s%s", home, CACHE_DIR_DEFAULT);
+ return path;
+}
+
+int mkdir_recursive(const char *path) {
+ char tmp[PATH_MAX];
+ char *p = NULL;
+ size_t len;
+
+ snprintf(tmp, sizeof(tmp), "%s", path);
+ len = strlen(tmp);
+ if (tmp[len - 1] == '/') {
+ tmp[len - 1] = '\0';
+ }
+
+ for (p = tmp + 1; *p; p++) {
+ if (*p == '/') {
+ *p = '\0';
+ mkdir(tmp, 0755);
+ *p = '/';
+ }
+ }
+ return mkdir(tmp, 0755);
+}
+
+char *trim_whitespace(char *str) {
+ while (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r') str++;
+ if (*str == '\0') return str;
+ char *end = str + strlen(str) - 1;
+ while (end > str && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) {
+ *end = '\0';
+ end--;
+ }
+ return str;
+}
diff --git a/src/common/fs-utils.h b/src/common/fs-utils.h
@@ -0,0 +1,14 @@
+#ifndef FS_UTILS_H
+#define FS_UTILS_H
+
+#include <stddef.h>
+
+#define REPO_DIR_DEFAULT "/.config/finwo/dep/repositories.d/"
+#define CACHE_DIR_DEFAULT "/.config/finwo/dep/repositories.cache/"
+
+char *get_repo_dir(void);
+char *get_cache_dir(void);
+int mkdir_recursive(const char *path);
+char *trim_whitespace(char *str);
+
+#endif // FS_UTILS_H
diff --git a/src/common/github-utils.c b/src/common/github-utils.c
@@ -0,0 +1,63 @@
+#include "github-utils.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "net-utils.h"
+#include "tidwall/json.h"
+
+static char *github_ref(const char *full_name, const char *ref_type, const char *ref) {
+ char url[512];
+ snprintf(url, sizeof(url), "https://api.github.com/repos/%s/git/ref/%s/%s", full_name, ref_type, ref);
+
+ size_t size;
+ char *response = download_url(url, &size);
+ if (!response) return NULL;
+
+ struct json root = json_parse(response);
+ struct json ref_obj = json_object_get(root, "ref");
+
+ char *full_ref = NULL;
+ if (json_exists(ref_obj) && json_type(ref_obj) == JSON_STRING) {
+ size_t len = json_string_length(ref_obj);
+ full_ref = malloc(len + 1);
+ if (full_ref) {
+ json_string_copy(ref_obj, full_ref, len + 1);
+ }
+ }
+
+ free(response);
+ return full_ref;
+}
+
+char *github_default_branch(const char *full_name) {
+ char url[512];
+ snprintf(url, sizeof(url), "https://api.github.com/repos/%s", full_name);
+
+ size_t size;
+ char *response = download_url(url, &size);
+ if (!response) return NULL;
+
+ struct json root = json_parse(response);
+ struct json default_branch = json_object_get(root, "default_branch");
+
+ char *branch = NULL;
+ if (json_exists(default_branch) && json_type(default_branch) == JSON_STRING) {
+ size_t len = json_string_length(default_branch);
+ branch = malloc(len + 1);
+ if (branch) {
+ json_string_copy(default_branch, branch, len + 1);
+ }
+ }
+
+ free(response);
+ return branch;
+}
+
+char *github_matching_ref(const char *full_name, const char *ref) {
+ char *full_ref = github_ref(full_name, "tags", ref);
+ if (full_ref) return full_ref;
+
+ return github_ref(full_name, "heads", ref);
+}
diff --git a/src/common/github-utils.h b/src/common/github-utils.h
@@ -0,0 +1,11 @@
+#ifndef GITHUB_UTILS_H
+#define GITHUB_UTILS_H
+
+#include <stddef.h>
+
+#include "net-utils.h"
+
+char *github_default_branch(const char *full_name);
+char *github_matching_ref(const char *full_name, const char *ref);
+
+#endif // GITHUB_UTILS_H
diff --git a/src/common/net-utils.c b/src/common/net-utils.c
@@ -0,0 +1,283 @@
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "emmanuel-marty/em_inflate.h"
+#include "erkkah/naett.h"
+#include "rxi/microtar.h"
+#include "tidwall/json.h"
+
+/* Forward declarations for helpers */
+static int mem_read(mtar_t *tar, void *data, unsigned size);
+static int mem_seek(mtar_t *tar, unsigned pos);
+static int mem_close(mtar_t *tar);
+
+/* Membuffer structure used by the tar memory stream */
+typedef struct {
+ char *data;
+ size_t size;
+ size_t pos;
+} membuffer_t;
+
+/* Check if a string looks like a URL */
+int is_url(const char *str) {
+ return strncmp(str, "http://", 7) == 0 || strncmp(str, "https://", 8) == 0;
+}
+
+static int mkdir_recursive(const char *path) {
+ char tmp[PATH_MAX];
+ char *p = NULL;
+ size_t len;
+
+ snprintf(tmp, sizeof(tmp), "%s", path);
+ len = strlen(tmp);
+ if (tmp[len - 1] == '/') {
+ tmp[len - 1] = '\0';
+ }
+
+ for (p = tmp + 1; *p; p++) {
+ if (*p == '/') {
+ *p = '\0';
+ mkdir(tmp, 0755);
+ *p = '/';
+ }
+ }
+ return mkdir(tmp, 0755);
+}
+
+/* Static helper for downloading with retries */
+static char *download_url_with_retry(const char *url, size_t *out_size, int retries) {
+ static int naett_initialized = 0;
+
+ if (!naett_initialized) {
+ naettInit(NULL);
+ naett_initialized = 1;
+ }
+
+ naettReq *req = naettRequest(url, naettMethod("GET"), naettTimeout(30000));
+ if (!req) {
+ fprintf(stderr, "Error: failed to create request\n");
+ return NULL;
+ }
+
+ naettRes *res = naettMake(req);
+ if (!res) {
+ naettFree(req);
+ fprintf(stderr, "Error: failed to make request\n");
+ return NULL;
+ }
+
+ while (!naettComplete(res)) {
+ usleep(10000);
+ }
+
+ int status = naettGetStatus(res);
+ const char *remaining = naettGetHeader(res, "X-RateLimit-Remaining");
+ int body_size = 0;
+ const void *body = naettGetBody(res, &body_size);
+
+ if (status == 403 || status == 429 || status == 404) {
+ if (remaining && strcmp(remaining, "0") == 0) {
+ if (retries > 0) {
+ fprintf(stderr, "Rate limited, waiting 5 seconds before retry...\n");
+ naettClose(res);
+ naettFree(req);
+ sleep(5);
+ return download_url_with_retry(url, out_size, retries - 1);
+ }
+ }
+ }
+
+ if (status != 200) {
+ fprintf(stderr, "Error: HTTP status %d for %s\n", status, url);
+ naettClose(res);
+ naettFree(req);
+ return NULL;
+ }
+
+ if (!body || body_size == 0) {
+ if (retries > 0) {
+ fprintf(stderr, "Empty response, waiting 5 seconds before retry...\n");
+ naettClose(res);
+ naettFree(req);
+ sleep(5);
+ return download_url_with_retry(url, out_size, retries - 1);
+ }
+ fprintf(stderr, "Error: empty response body\n");
+ naettClose(res);
+ naettFree(req);
+ return NULL;
+ }
+
+ char *data = malloc(body_size);
+ if (data) {
+ memcpy(data, body, body_size);
+ *out_size = body_size;
+ }
+
+ naettClose(res);
+ naettFree(req);
+
+ return data;
+}
+
+/* Public download URL function */
+char *download_url(const char *url, size_t *out_size) {
+ return download_url_with_retry(url, out_size, 3);
+}
+
+/* Memory tar callbacks */
+static int mem_read(mtar_t *tar, void *data, unsigned size) {
+ membuffer_t *buf = (membuffer_t *)tar->stream;
+ if (buf->pos + size > buf->size) {
+ return MTAR_EREADFAIL;
+ }
+ memcpy(data, buf->data + buf->pos, size);
+ buf->pos += size;
+ return MTAR_ESUCCESS;
+}
+
+static int mem_seek(mtar_t *tar, unsigned pos) {
+ membuffer_t *buf = (membuffer_t *)tar->stream;
+ if (pos > buf->size) {
+ return MTAR_ESEEKFAIL;
+ }
+ buf->pos = pos;
+ return MTAR_ESUCCESS;
+}
+
+static int mem_close(mtar_t *tar) {
+ (void)tar;
+ return MTAR_ESUCCESS;
+}
+
+/* Download and extract a tar.gz URL into a directory */
+int download_and_extract(const char *url, const char *dest_dir) {
+ size_t gzip_size;
+ char *gzip_data = download_url(url, &gzip_size);
+ if (!gzip_data) {
+ return -1;
+ }
+
+ if (gzip_size < 10 || (unsigned char)gzip_data[0] != 0x1f || (unsigned char)gzip_data[1] != 0x8b) {
+ free(gzip_data);
+ fprintf(stderr, "Error: downloaded data is not gzip format\n");
+ return -1;
+ }
+
+ size_t max_tar_size = gzip_size * 15;
+ char *tar_data = malloc(max_tar_size);
+ if (!tar_data) {
+ free(gzip_data);
+ return -1;
+ }
+
+ size_t tar_size = em_inflate(gzip_data, gzip_size, (unsigned char *)tar_data, max_tar_size);
+ free(gzip_data);
+
+ if (tar_size == (size_t)-1) {
+ free(tar_data);
+ fprintf(stderr, "Error: decompression failed (invalid or corrupted gzip data)\n");
+ return -1;
+ }
+ if (tar_size == 0) {
+ free(tar_data);
+ fprintf(stderr, "Error: decompressed to empty (likely wrong format or truncated data)\n");
+ return -1;
+ }
+
+ membuffer_t membuf = {.data = tar_data, .size = tar_size, .pos = 0};
+
+ mtar_t tar;
+ memset(&tar, 0, sizeof(tar));
+ tar.read = mem_read;
+ tar.seek = mem_seek;
+ tar.close = mem_close;
+ tar.stream = &membuf;
+
+ char first_component[256] = {0};
+ int first_component_found = 0;
+
+ while (1) {
+ mtar_header_t h;
+ int err = mtar_read_header(&tar, &h);
+ if (err == MTAR_ENULLRECORD) break;
+ if (err != MTAR_ESUCCESS) {
+ fprintf(stderr, "Error reading tar header: %s\n", mtar_strerror(err));
+ break;
+ }
+
+ if (!first_component_found && (h.type == MTAR_TREG || h.type == MTAR_TDIR)) {
+ char *slash = strchr(h.name, '/');
+ if (slash) {
+ size_t len = slash - h.name;
+ strncpy(first_component, h.name, len);
+ first_component[len] = '\0';
+ first_component_found = 1;
+ }
+ }
+
+ char full_path[PATH_MAX];
+ char *name_ptr = h.name;
+
+ if (first_component_found) {
+ size_t first_len = strlen(first_component);
+ if (strncmp(h.name, first_component, first_len) == 0 && h.name[first_len] == '/') {
+ name_ptr = h.name + first_len + 1;
+ }
+ }
+
+ if (strlen(name_ptr) == 0) {
+ mtar_next(&tar);
+ continue;
+ }
+
+ snprintf(full_path, sizeof(full_path), "%s/%s", dest_dir, name_ptr);
+
+ if (h.type == MTAR_TDIR) {
+ mkdir_recursive(full_path);
+ } else if (h.type == MTAR_TREG) {
+ char *last_slash = strrchr(full_path, '/');
+ if (last_slash) {
+ *last_slash = '\0';
+ mkdir_recursive(full_path);
+ *last_slash = '/';
+ }
+
+ // Skip if file already exists
+ if (access(full_path, F_OK) == 0) {
+ mtar_next(&tar);
+ continue;
+ }
+
+ FILE *f = fopen(full_path, "wb");
+ if (!f) {
+ fprintf(stderr, "Error: could not create file %s\n", full_path);
+ mtar_next(&tar);
+ continue;
+ }
+
+ char buf[8192];
+ unsigned remaining = h.size;
+ while (remaining > 0) {
+ unsigned to_read = remaining > sizeof(buf) ? sizeof(buf) : remaining;
+ int read_err = mtar_read_data(&tar, buf, to_read);
+ if (read_err != MTAR_ESUCCESS) {
+ fprintf(stderr, "Error reading tar data\n");
+ break;
+ }
+ fwrite(buf, 1, to_read, f);
+ remaining -= to_read;
+ }
+ fclose(f);
+ }
+
+ mtar_next(&tar);
+ }
+
+ free(tar_data);
+ return 0;
+}
diff --git a/src/common/net-utils.h b/src/common/net-utils.h
@@ -0,0 +1,10 @@
+#ifndef NET_UTILS_H
+#define NET_UTILS_H
+
+#include <stddef.h>
+
+int is_url(const char *str);
+char *download_url(const char *url, size_t *out_size);
+int download_and_extract(const char *url, const char *dest_dir);
+
+#endif // NET_UTILS_H
diff --git a/src/main.c b/src/main.c
@@ -0,0 +1,79 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "cofyc/argparse.h"
+#include "command/command.h"
+#include "erkkah/naett.h"
+#include "rxi/microtar.h"
+
+#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
+
+struct cmd_struct *commands = NULL;
+
+static void print_global_usage(void) {
+ printf("Usage: dep [global options] <command> [command options]\n");
+ printf("\n");
+ printf("Global options:\n");
+ printf(" n/a\n");
+ printf("\n");
+ printf("Commands:\n");
+
+ struct cmd_struct *cmd = commands;
+ while (cmd) {
+ printf(" %-16s %s\n", cmd->display ? cmd->display : cmd->name[0], cmd->description ? cmd->description : "");
+ cmd = cmd->next;
+ }
+
+ printf("\n");
+ printf("Help topics:\n");
+ printf(" global This help text\n");
+
+ cmd = commands;
+ while (cmd) {
+ printf(" %-16s More detailed explanation on the %s command\n", cmd->name[0], cmd->name[0]);
+ cmd = cmd->next;
+ }
+}
+
+static const char *const usages[] = {
+ "dep [global options] <command> [command options]",
+ NULL,
+};
+
+int main(int argc, const char **argv) {
+ struct argparse argparse;
+ struct argparse_option options[] = {
+ OPT_HELP(),
+ OPT_END(),
+ };
+ argparse_init(&argparse, options, usages, ARGPARSE_STOP_AT_NON_OPTION);
+ argc = argparse_parse(&argparse, argc, argv);
+ if (argc < 1) {
+ print_global_usage();
+ return 0;
+ }
+
+ /* Try to run command with args provided. */
+ struct cmd_struct *cmd = commands;
+ while (cmd) {
+ const char **name = cmd->name;
+ while (*name) {
+ if (!strcmp(*name, argv[0])) {
+ goto found;
+ }
+ name++;
+ }
+ cmd = cmd->next;
+ }
+found:
+
+ if (cmd) {
+ return cmd->fn(argc, argv);
+ } else {
+ fprintf(stderr, "Unknown command: %s\n", argv[0]);
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/src/main.sh b/src/main.sh
@@ -1,40 +0,0 @@
-cmds=("")
-# #include "command/help/index.sh"
-# #include "command/add/index.sh"
-# #include "command/install/index.sh"
-# #include "command/repo/index.sh"
-
-function main {
- cmd=help
-
- while [ "$#" -gt 0 ]; do
-
- # If argument is a command, pass parsing on to it & stop main parser
- if [[ " ${cmds[*]} " =~ " $1 " ]]; then
- cmd=$1
- shift
- arg_$cmd "$@"
- break
- fi
-
- # Main parser
- case "$1" in
- --)
- shift
- break 2
- ;;
- *)
- echo "Unknown argument: $1" >&2
- exit 1
- ;;
- esac
- shift
-
- done
-
- cmd_$cmd
-}
-
-if [ $(basename $0) == "__NAME" ]; then
- main "$@"
-fi
diff --git a/src/util/ini.sh b/src/util/ini.sh
@@ -1,103 +0,0 @@
-# #ifndef __INI_SH__
-# #define __INI_SH__
-
-# #include "shopt.sh"
-
-# Arguments:
-# $0 <fn_keyHandler> <str_filename> [section[.key]]
-function ini_foreach {
-
- # No file = no data
- inifile="${2}"
- if [[ ! -f "$inifile" ]]; then
- exit 1
- fi
-
- # Process the file line-by-line
- SECTION=
- while read line; do
-
- # Fix newlines
- line=$(echo $line | tr -d '\015')
-
- # Remove surrounding whitespace
- line=${line##*( )} # From the beginning
- line=${line%%*( )} # From the end
-
- # Remove comments and empty lines
- if [[ "${line:0:1}" == '#' ]] || [[ "${line:0:1}" == ';' ]] || [[ "${#line}" == 0 ]]; then
- continue
- fi
-
- # Handle section markers
- if [[ "${line:0:1}" == "[" ]]; then
- SECTION=$(echo $line | sed -e 's/\[\(.*\)\]/\1/')
- SECTION=${SECTION##*( )}
- SECTION=${SECTION%%*( )}
- SECTION="${SECTION}."
- continue
- fi
-
- # Output found variable
- NAME=${line%%=*}
- NAME=${NAME%%*( )}
- VALUE=${line#*=}
- VALUE=${VALUE##*( )}
-
- # Output searched or all
- if [[ -z "${3}" ]]; then
- $1 "$SECTION" "$NAME" "${VALUE}"
- elif [[ "${SECTION}" == "${3}" ]] || [[ "${SECTION}${NAME}" == "${3}" ]]; then
- $1 "$SECTION" "$NAME" "${VALUE}"
- fi
-
- done < "${inifile}"
-}
-
-function ini_write {
- PREVIOUSSECTION=
- echo -en "" > "$1"
- while read line; do
- KEYFULL=${line%%=*}
- VALUE=${line#*=}
- SECTION=${KEYFULL%%.*}
- KEY=${KEYFULL#*.}
- if [[ "${SECTION}" != "${PREVIOUSSECTION}" ]]; then
- if [ ! -z "${PREVIOUSSECTION}" ]; then
- echo "" >> "$1"
- fi
- echo "[${SECTION}]" >> "$1"
- PREVIOUSSECTION="${SECTION}"
- fi
- echo "${KEY}=${VALUE}" >> "$1"
- done < <(sort --unique)
-}
-
-function ini_output_full {
- echo "$1$2=$3"
-}
-function ini_output_section {
- echo "$2=$3"
-}
-function ini_output_value {
- echo "$3"
-}
-
-# Allow this file to be called stand-alone
-# ini.sh <filename> [section[.key]] [sectionmode]
-if [ $(basename $0) == "ini.sh" ]; then
- fullMode=full
- sectionMode=value
- if [[ ! -z "${3}" ]]; then
- fullMode=${3}
- sectionMode=${3}
- fi
- if [[ -z "${2}" ]]; then
- ini_foreach ini_output_${fullMode} "$@"
- else
- ini_foreach ini_output_${sectionMode} "$@"
- fi
-fi
-
-# __INI_SH__
-# #endif
diff --git a/src/util/ostype.sh b/src/util/ostype.sh
@@ -1,16 +0,0 @@
-# #ifndef __OSTYPE_SH__
-# #define __OSTYPE_SH__
-
-function ostype {
- case "$OSTYPE" in
- darwin*) echo "osx" ;;
- linux*) echo "lin" ;;
- bsd*) echo "bsd" ;;
- msys*) echo "win" ;;
- cygwin*) echo "win" ;;
- *) echo "unknown" ;;
- esac
-}
-
-# __OSTYPE_SH__
-# #endif
diff --git a/src/util/shopt.sh b/src/util/shopt.sh
@@ -1,8 +0,0 @@
-# #ifndef __SHOPT_SH__
-# #define __SHOPT_SH__
-
-# Required for the whitespace trimming
-shopt -s extglob
-
-# __SHOPT_SH__
-# #endif