✍️ 필사 모드: The Complete Guide to Package Managers — How npm, uv, RPM, and Homebrew Work and How to Publish Software
EnglishIntroduction
Package managers are the backbone of software development infrastructure. Behind the everyday commands we use, like npm install, pip install, brew install, and yum install, lie complex mechanisms for dependency resolution, version management, and binary distribution.
In this post, we dissect the internals of four major package managers and walk through how to register your own software in each ecosystem.
Part 1: npm Internals (JavaScript / Node.js)
1-1. What Is npm?
npm (Node Package Manager) is the standard package manager for the JavaScript ecosystem. It has three core components.
Registry: The central repository where all packages are stored. Hosted at registry.npmjs.org and distributed worldwide via CDN. As of 2026, over 3 million packages are registered.
package.json: The file that declares project metadata and dependencies.
{
"name": "my-project",
"version": "1.0.0",
"dependencies": {
"express": "^4.18.0",
"lodash": "~4.17.21"
},
"devDependencies": {
"jest": "^29.0.0"
}
}
node_modules: The directory where dependencies are actually installed. npm uses a nested structure by default but flattens it as much as possible through hoisting.
1-2. Dependency Resolution
npm's dependency resolution can be explained through four key concepts.
Semver (Semantic Versioning):
npm follows semver rules. Versions use the MAJOR.MINOR.PATCH format, and each range specifier has a specific meaning.
| Specifier | Meaning | Example |
|---|---|---|
^4.18.0 | Fixed MAJOR, allow MINOR and PATCH | 4.18.0 up to but not 5.0.0 |
~4.17.21 | Fixed MAJOR and MINOR, allow PATCH only | 4.17.21 up to but not 4.18.0 |
4.18.0 | Exactly that version | 4.18.0 only |
>=4.0.0 | That version and above | 4.0.0 and higher |
Lock Files:
package-lock.json records the exact version, integrity hash, and resolved URL of every installed package. This ensures the entire team can reproduce identical dependency trees.
{
"name": "my-project",
"lockfileVersion": 3,
"packages": {
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-abc123..."
}
}
}
Hoisting:
If package A depends on lodash 4.17.21 and package B also depends on lodash 4.17.21, npm installs lodash only once in the top-level node_modules. However, if different versions are required, duplicates are installed in nested node_modules directories.
Phantom Dependencies:
Because of hoisting, you can import packages that your project does not directly depend on. A package may be usable simply because it was hoisted by another dependency, even though it is not declared in your package.json. This leads to breakages when the dependency tree changes.
1-3. npm vs yarn vs pnpm
Here is a comparison of the three main package managers.
| Feature | npm | yarn (Berry) | pnpm |
|---|---|---|---|
| Storage | node_modules (hoisted) | Plug'n'Play (PnP) | content-addressable store + symlinks |
| Lock file | package-lock.json | yarn.lock | pnpm-lock.yaml |
| Phantom dep prevention | No | Yes (strict PnP) | Yes (isolated node_modules) |
| Disk usage | High | Low | Very low (hard links) |
| Workspaces | npm workspaces | yarn workspaces | pnpm workspaces |
| Performance | Average | Good | Very good |
The Core Idea of pnpm:
pnpm stores packages once in a global content-addressable store and creates hard links in each project's node_modules. Even if 10 projects use the same version of lodash, it is stored only once on disk.
~/.pnpm-store/
v3/
files/
ab/cdef1234... # actual files of lodash 4.17.21
project-a/node_modules/.pnpm/
lodash@4.17.21/
node_modules/
lodash/
index.js --> ~/.pnpm-store/v3/files/ab/cdef1234... (hard link)
1-4. Publishing a Package to npm
Here is the step-by-step process for publishing your package to the npm registry.
Step 1: Create an Account and Log In
npm adduser
# or if you already have an account
npm login
Step 2: Initialize the Package
mkdir my-awesome-lib
cd my-awesome-lib
npm init
Step 3: Complete package.json
{
"name": "my-awesome-lib",
"version": "1.0.0",
"description": "A library that does awesome things",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
},
"keywords": ["awesome", "utility"],
"author": "Your Name",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/yourname/my-awesome-lib"
}
}
Step 4: Publish
# Verify the build
npm run build
# Preview before publishing (check which files are included)
npm pack --dry-run
# Actually publish
npm publish
# For scoped packages (public)
npm publish --access public
Step 5: Version Management
# Bump patch version (1.0.0 -> 1.0.1)
npm version patch
# Bump minor version (1.0.1 -> 1.1.0)
npm version minor
# Bump major version (1.1.0 -> 2.0.0)
npm version major
# Publish
npm publish
Part 2: uv Internals (Python)
2-1. What Is uv?
uv is an ultra-fast Python package manager and project management tool built in Rust by Astral. It replaces pip while delivering 10x to 100x faster performance.
The reasons uv is fast include:
- Written in Rust: Compiled to a native binary with no Python interpreter overhead
- Parallel downloads: Dependency resolution and downloads happen concurrently
- Global cache: Packages downloaded once are reused across all projects
- Optimized SAT solver: Efficiently resolves the dependency graph
# Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
# Create a project
uv init my-project
cd my-project
# Add dependencies
uv add requests flask
# Sync dependencies (based on lock file)
uv sync
2-2. pip vs uv vs poetry vs conda
| Feature | pip | uv | poetry | conda |
|---|---|---|---|---|
| Language | Python | Rust | Python | Python/C |
| Dependency resolution | Backtracking | SAT solver | SAT solver | SAT solver |
| Lock file | None (manual freeze) | uv.lock | poetry.lock | environment.yml |
| Virtual env management | No (separate venv) | Yes (built-in) | Yes (built-in) | Yes (built-in) |
| Speed (cold install) | Slow (baseline) | 10-100x faster | 2-5x faster | Slow |
| Build system | setuptools | Self-resolving | Self-building | Self-building |
| Non-Python packages | No | No | No | Yes (numpy C libs, etc.) |
Speed comparison (real benchmark):
# Installing requests + flask + sqlalchemy (cold cache)
pip install: 12.4s
poetry install: 8.1s
uv sync: 0.8s # 15x faster
2-3. How uv Resolves Dependencies
uv uses a SAT solver based on the PubGrub algorithm. Here is the step-by-step process.
1. Build the Dependency Graph:
Starting from the project's direct dependencies, uv reads each package's metadata to construct the full transitive dependency graph.
2. Constraint Propagation:
Each package's version requirements are converted into constraints, which are propagated to narrow down the possible version space.
3. Unit Propagation:
When a variable has only one possible value remaining, that value is fixed and related constraints are updated.
4. Conflict-Driven Clause Learning (CDCL):
When a conflict occurs, uv analyzes its root cause and adds a "learned clause" to avoid repeating the same failure.
Example: A>=1.0 requires B>=2.0, but C<1.5 requires B<2.0
-> Conflict detected
-> Learned: A>=1.0 AND C<1.5 cannot hold simultaneously
-> Backtrack and try different versions
2-4. Publishing a Package to PyPI
Here is the modern approach to registering a Python package on PyPI (Python Package Index).
Step 1: Write pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-python-lib"
version = "1.0.0"
description = "A useful Python library"
readme = "README.md"
requires-python = ">=3.9"
license = "MIT"
authors = [
{ name = "Your Name", email = "you@example.com" }
]
dependencies = [
"requests>=2.28.0",
"pydantic>=2.0",
]
[project.urls]
Homepage = "https://github.com/yourname/my-python-lib"
Documentation = "https://my-python-lib.readthedocs.io"
[project.optional-dependencies]
dev = ["pytest", "ruff", "mypy"]
Step 2: Build
# Install build tools
uv add --dev build
# Run the build
python -m build
# .whl and .tar.gz files are created in the dist/ directory
ls dist/
# my_python_lib-1.0.0-py3-none-any.whl
# my_python_lib-1.0.0.tar.gz
Step 3: Test on TestPyPI
# Install twine
uv add --dev twine
# Upload to TestPyPI
python -m twine upload --repository testpypi dist/*
# Test installation
pip install --index-url https://test.pypi.org/simple/ my-python-lib
Step 4: Publish to PyPI
# Upload to PyPI
python -m twine upload dist/*
# Now anyone can install it
pip install my-python-lib
# or
uv add my-python-lib
Trusted Publisher Setup (GitHub Actions):
name: Publish to PyPI
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install build
- run: python -m build
- uses: pypa/gh-action-pypi-publish@release/v1
This approach lets you publish to PyPI without API tokens, using GitHub OIDC tokens for authentication.
Part 3: RPM (Red Hat / CentOS / Rocky Linux)
3-1. What Is RPM?
RPM (Red Hat Package Manager) is the package management system for Red Hat-based Linux distributions. Its core components are as follows.
RPM Files: Binary package files with the .rpm extension. They contain compiled programs, configuration files, documentation, and install/uninstall scripts.
RPM Database: Located at /var/lib/rpm, it tracks information about all installed packages.
yum / dnf: Higher-level tools that solve RPM's dependency resolution problem. RPM can only install individual packages, but dnf automatically resolves dependencies and downloads packages from remote repositories.
# Direct RPM usage (no automatic dependency resolution)
rpm -ivh package-1.0.0-1.el9.x86_64.rpm
# Using dnf (automatic dependency resolution)
dnf install nginx
# Query package info
rpm -qi nginx
# List files in a package
rpm -ql nginx
Spec Files: Recipe files for building RPM packages. They define how to compile source code, which files to install where, and what dependencies are needed.
3-2. Building an RPM Package
Step 1: Prepare the Build Environment
# Install build tools
dnf install rpm-build rpmdevtools
# Create the build directory structure
rpmdev-setuptree
# The resulting structure:
# ~/rpmbuild/
# BUILD/ - where builds are performed
# RPMS/ - built RPM files
# SOURCES/ - source tarballs
# SPECS/ - spec files
# SRPMS/ - source RPM files
Step 2: Write a Spec File
Name: myapp
Version: 1.0.0
Release: 1%{?dist}
Summary: My awesome application
License: MIT
URL: https://github.com/yourname/myapp
Source0: %{name}-%{version}.tar.gz
BuildRequires: gcc
BuildRequires: make
Requires: openssl-libs
%description
MyApp is an awesome application that does useful things.
It supports multiple platforms and is easy to configure.
%prep
%autosetup
%build
%configure
%make_build
%install
%make_install
%files
%license LICENSE
%doc README.md
%{_bindir}/myapp
%{_mandir}/man1/myapp.1*
%config(noreplace) %{_sysconfdir}/myapp.conf
%changelog
* Sat Apr 12 2026 Your Name <you@example.com> - 1.0.0-1
- Initial package
Step 3: Build
# Copy source tarball to SOURCES
cp myapp-1.0.0.tar.gz ~/rpmbuild/SOURCES/
# Build RPM (-ba: both binary and source RPM)
rpmbuild -ba ~/rpmbuild/SPECS/myapp.spec
# Check the built RPM
ls ~/rpmbuild/RPMS/x86_64/
# myapp-1.0.0-1.el9.x86_64.rpm
Step 4: Create a Local Repository
# Install createrepo
dnf install createrepo_c
# Create the repo directory
mkdir -p /var/www/html/myrepo/
# Copy RPMs
cp ~/rpmbuild/RPMS/x86_64/myapp-*.rpm /var/www/html/myrepo/
# Generate repo metadata
createrepo /var/www/html/myrepo/
# /etc/yum.repos.d/myrepo.repo
[myrepo]
name=My Custom Repository
baseurl=http://myserver.example.com/myrepo/
enabled=1
gpgcheck=0
3-3. DEB vs RPM Comparison
| Feature | RPM (Red Hat family) | DEB (Debian family) |
|---|---|---|
| Distributions | RHEL, CentOS, Rocky, Fedora | Debian, Ubuntu, Mint |
| Package format | .rpm | .deb |
| Low-level tool | rpm | dpkg |
| High-level tool | yum / dnf | apt / apt-get |
| Package definition | spec file | debian/ directory (control, rules, etc.) |
| Build tool | rpmbuild | dpkg-buildpackage |
| Repo creation | createrepo | apt-ftparchive / reprepro |
| Script stages | pre/post install/uninstall | preinst/postinst/prerm/postrm |
| Signing | GPG | GPG (apt-key) |
The key difference lies in design philosophy. RPM's spec file puts everything in one file, while DEB's debian/ directory separates concerns into different files by role.
Part 4: Homebrew (macOS / Linux)
4-1. How Homebrew Works
Homebrew is the unofficial package manager for macOS (and Linux). Here are the core concepts.
Formula: A Ruby script that defines how to install a package. It includes the source URL, build options, dependencies, and installation steps.
Tap: A Git repository that contains a collection of Formulae. The default Tap is homebrew-core, and anyone can create their own Tap.
Cellar: The location where packages are actually installed. On macOS, this is /opt/homebrew/Cellar/ (Apple Silicon) or /usr/local/Cellar/ (Intel).
Keg-only: A package installed in the Cellar but not symlinked into PATH. This prevents conflicts when the system already has the same program. A classic example is openssl.
# Install a package
brew install wget
# Check the install path
brew --prefix wget
# /opt/homebrew/opt/wget
# Cellar internal structure
ls /opt/homebrew/Cellar/wget/1.21.4/
# bin/ etc/ share/
# Force-link a keg-only package
brew link --force openssl@3
Bottle: A pre-compiled binary package. Downloading a Bottle instead of building from source dramatically speeds up installation. Most official Formulae provide Bottles for both macOS and Linux.
4-2. Registering Software with Homebrew
There are three ways to register software with Homebrew.
Method 1: Create a Personal Tap
The simplest method with the fewest restrictions.
Step 1: Create a GitHub Repository
Create a repository named homebrew-mytap. Homebrew recognizes the homebrew- prefix as a Tap name.
Step 2: Write a Formula
# Formula/myapp.rb
class Myapp < Formula
desc "My awesome command-line application"
homepage "https://github.com/yourname/myapp"
url "https://github.com/yourname/myapp/archive/refs/tags/v1.0.0.tar.gz"
sha256 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
license "MIT"
depends_on "go" => :build
def install
system "go", "build", *std_go_args(ldflags: "-s -w -X main.version=#{version}")
end
test do
assert_match "myapp version #{version}", shell_output("#{bin}/myapp --version")
end
end
Step 3: Use It
# Add the Tap
brew tap yourname/mytap
# Install
brew install yourname/mytap/myapp
# Or, after adding the Tap, install directly
brew install myapp
Method 2: Submit a PR to homebrew-core
Getting included in the official Homebrew requires meeting strict criteria.
Requirements:
- At least 30 GitHub stars (or a substantial user base)
- Stable release tags
- An open-source license
- Automated builds via CI/CD
- Must build on both macOS and Linux
# Clone homebrew-core and add a Formula
brew tap --force homebrew/core
cd $(brew --repository homebrew/core)
# Formula creation helper
brew create https://github.com/yourname/myapp/archive/refs/tags/v1.0.0.tar.gz
# Validate the Formula
brew audit --new myapp
brew test myapp
# Submit PR (GitHub CLI)
gh pr create --title "myapp 1.0.0 (new formula)" --body "Description of the tool..."
Method 3: Cask (GUI Applications)
Used for distributing macOS GUI applications in .app, .dmg, or .pkg format.
# Casks/myguiapp.rb
cask "myguiapp" do
version "2.1.0"
sha256 "abc123def456..."
url "https://github.com/yourname/myguiapp/releases/download/v#{version}/MyGuiApp-#{version}.dmg"
name "MyGuiApp"
desc "A beautiful GUI application"
homepage "https://myguiapp.example.com"
app "MyGuiApp.app"
zap trash: [
"~/Library/Application Support/MyGuiApp",
"~/Library/Preferences/com.yourname.myguiapp.plist",
]
end
# Install a Cask
brew install --cask myguiapp
4-3. Formula Authoring in Detail
Homebrew Formulae are written in a Ruby DSL. Here are the key components.
class ComplexApp < Formula
desc "A complex application with many build options"
homepage "https://complexapp.dev"
# Stable version source
url "https://github.com/yourname/complexapp/archive/refs/tags/v2.0.0.tar.gz"
sha256 "deadbeef..."
# HEAD version (in development)
head "https://github.com/yourname/complexapp.git", branch: "main"
license "Apache-2.0"
# Build dependencies
depends_on "cmake" => :build
depends_on "pkg-config" => :build
# Runtime dependencies
depends_on "openssl@3"
depends_on "sqlite"
# Platform restriction
depends_on :macos
def install
args = %W[
--prefix=#{prefix}
--with-openssl=#{Formula["openssl@3"].opt_prefix}
--with-sqlite=#{Formula["sqlite"].opt_prefix}
]
system "./configure", *args
system "make", "install"
# Install shell completions
bash_completion.install "completions/complexapp.bash"
zsh_completion.install "completions/_complexapp"
fish_completion.install "completions/complexapp.fish"
end
# Post-install message
def caveats
<<~EOS
To start complexapp as a service:
brew services start complexapp
EOS
end
# Installation verification test
test do
assert_match version.to_s, shell_output("#{bin}/complexapp --version")
system "#{bin}/complexapp", "check"
end
end
Key Formula DSL Methods:
| Method | Purpose | Example |
|---|---|---|
url | Source download URL | url "https://..." |
sha256 | Integrity verification hash | sha256 "abc..." |
depends_on | Dependency declaration | depends_on "openssl@3" |
install | Build and install steps | system "make", "install" |
test | Installation verification | assert_match ... |
prefix | Base install path | /opt/homebrew/Cellar/app/1.0 |
bin | Executable path | prefix/"bin" |
etc | Config file path | prefix/"etc" |
share | Shared data path | prefix/"share" |
Part 5: The Grand Comparison
5-1. Comprehensive Comparison Matrix
| Feature | npm | PyPI (uv/pip) | RPM (dnf) | Homebrew | APT (deb) | snap | flatpak |
|---|---|---|---|---|---|---|---|
| Target | Node.js libraries | Python libraries | System packages | CLI/GUI apps | System packages | Desktop apps | Desktop apps |
| Platform | Cross-platform | Cross-platform | RHEL family | macOS/Linux | Debian family | Linux | Linux |
| Registry | npmjs.com | pypi.org | Vendor repos | homebrew-core | Vendor repos | snapcraft.io | flathub.org |
| Isolation | node_modules | virtualenv | None (system-wide) | Cellar + symlinks | None (system-wide) | Sandbox | Sandbox |
| Dep resolution | semver ranges | SAT solver | libsolv (SAT) | Built-in | APT solver | Self-managed | Runtime sharing |
| Auto-update | No | No | dnf-automatic | brew upgrade | unattended-upgrades | snapd (auto) | No |
| Security signing | npm signatures | GPG/Sigstore | GPG | Code signing (Cask) | GPG | Snap Store signing | Flathub signing |
| Size concern | None | None | None | None | None | Large (bundled) | Large (runtime) |
5-2. Which Package Manager Should You Use?
To distribute a JavaScript/TypeScript library: Use npm or GitHub Packages.
To distribute a Python library: Register on PyPI and let users install with uv or pip.
To distribute system-level packages for Linux servers: Build RPM packages (for RHEL family) or DEB packages (for Debian family).
To distribute a CLI tool for macOS: Write a Homebrew Formula and publish it in a personal Tap or submit a PR to homebrew-core.
To distribute a cross-platform desktop application: Consider snap or flatpak. snap has built-in auto-updates, while flatpak offers a more open ecosystem.
Conclusion
Package managers are far more than simple installation tools. They solve the NP-complete problem of dependency resolution in practical ways, safely distribute millions of packages, and serve as the circulatory system of the developer ecosystem.
Understanding the internals of each package manager helps you resolve dependency conflicts faster, manage caches more efficiently, and makes the process of distributing your own software much smoother.
Try registering your project on npm, PyPI, Homebrew, or RPM. The best way to experience how a package manager works is to build and publish a package yourself.
References
- npm Official Docs: https://docs.npmjs.com/
- uv Official Docs: https://docs.astral.sh/uv/
- RPM Packaging Guide: https://rpm-packaging-guide.github.io/
- Homebrew Formula Cookbook: https://docs.brew.sh/Formula-Cookbook
- PyPI Publishing Guide: https://packaging.python.org/
- PubGrub Algorithm: https://nex3.medium.com/pubgrub-2fb6470504f
- pnpm Official Docs: https://pnpm.io/
현재 단락 (1/395)
Package managers are the backbone of software development infrastructure. Behind the everyday comman...