dep

Package manager for embedded C libraries
git clone git://git.finwo.net/app/dep
Log | Files | Refs | README | LICENSE

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:
A.clang-format | 334+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A.github/workflows/release.yml | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
M.gitignore | 5++---
DLICENSE | 20--------------------
ALICENSE.md | 34++++++++++++++++++++++++++++++++++
MMakefile | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
MREADME.md | 152++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
DREADME.md.html | 91-------------------------------------------------------------------------------
Maction.yml | 53+++++++++++++++++++++++++++++++++++++++++++++++------
Ddist/dep | 577-------------------------------------------------------------------------------
Dpackage.ini | 4----
Dsrc/command/add/help.txt | 13-------------
Dsrc/command/add/index.sh | 69---------------------------------------------------------------------
Asrc/command/add/main.c | 420+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/command/command.h | 10++++++++++
Dsrc/command/help/index.sh | 33---------------------------------
Asrc/command/help/main.c | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/command/help/topic/global.txt | 16----------------
Asrc/command/init/main.c | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/command/install/help.txt | 7-------
Dsrc/command/install/index.sh | 183-------------------------------------------------------------------------------
Asrc/command/install/main.c | 554+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/command/license/license.h | 7+++++++
Asrc/command/license/main.c | 34++++++++++++++++++++++++++++++++++
Asrc/command/license/main.h | 0
Dsrc/command/repo/help.txt | 8--------
Dsrc/command/repo/index.sh | 102-------------------------------------------------------------------------------
Asrc/command/repository/main.c | 339+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/common/fs-utils.c | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/common/fs-utils.h | 14++++++++++++++
Asrc/common/github-utils.c | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/common/github-utils.h | 11+++++++++++
Asrc/common/net-utils.c | 283+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/common/net-utils.h | 10++++++++++
Asrc/main.c | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/main.sh | 40----------------------------------------
Dsrc/util/ini.sh | 103-------------------------------------------------------------------------------
Dsrc/util/ostype.sh | 16----------------
Dsrc/util/shopt.sh | 8--------
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