Skip to content

Only hardlink if the source is a hardlink. - #2103

Merged
jordansissel merged 5 commits into
mainfrom
issue/2102
Sep 27, 2025
Merged

Only hardlink if the source is a hardlink.#2103
jordansissel merged 5 commits into
mainfrom
issue/2102

Conversation

@jordansissel

Copy link
Copy Markdown
Owner

In #2102, it was found that fpm would hardlink any file that's included in the package multiple times.

For example:

% echo "Hello world" > /tmp/one

# Create a package with /tmp/one installed in two locations using path mapping:
% fpm -s dir -t deb -n example /tmp/one=/opt/example/one /tmp/one=/var/lib/example/one

% dpkg-deb --fsys-tarfile example_1.0_amd64.deb| tar -vt | grep 'one'
-rw-r--r-- 0/0              12 2025-09-21 21:43 ./var/lib/example/one
hrw-r--r-- 0/0               0 2025-09-21 21:43 ./opt/example/one link to ./var/lib/example/one

Notice above, /opt/example/one is a hardlink to /var/lib/example/one.

This behavior is not the intended behavior of fpm. The intended behavior (introduced in #365) was to ensure hardlinks in the original files are correctly kept as hardlinks in the package. The example in #365 was for git-core which has over 100 files hardlinked to the git executable such as git-add, git-apply, etc. The problem in #365 was that fpm was copying these as individual files which resulted in hundreds of copies of the same file, and in git's case, the git binary is 18 megs.

Now, in this PR, the intent is to retain the solution to #365 (#623) while removing the unintended behavior.

# Copy /etc/zshrc into a two locations.
% fpm -s dir -t deb -n example /etc/zshrc=/zshrc1 /etc/zshrc=/zshrc2

% dpkg-deb --fsys-tarfile example_1.0_amd64.deb | tar -vt | grep /zshrc
-rw-r--r-- 0/0            1164 2025-01-18 16:00 ./zshrc2
hrw-r--r-- 0/0               0 2025-01-18 16:00 ./zshrc1 link to ./zshrc2

The above shows the buggy behavior.

With this PR, the package created looks like this:

% dpkg-deb --fsys-tarfile example_1.0_amd64.deb | tar -vt | grep /zshrc
-rw-r--r-- 0/0            1164 2025-01-18 16:00 ./zshrc2
-rw-r--r-- 0/0            1164 2025-01-18 16:00 ./zshrc1

Correctly two files, not hardlinked.

@jordansissel

Copy link
Copy Markdown
Owner Author

At time of writing, this PR still has a small bug that if you twice include a file that's hardlinked (stat reports nlink > 1), fpm will hardlink it. This is a bug, but I'm not sure how critical it is.

Example:

% ls -li /tmp/a
total 8
233022 -rw-r--r--. 2 jls jls 6 Sep 21 21:57 bar
233022 -rw-r--r--. 2 jls jls 6 Sep 21 21:57 foo

% fpm -s dir -t deb -n example /tmp/a/foo=/foo /tmp/a/foo=/foo2

% dpkg-deb --fsys-tarfile example_1.0_amd64.deb | tar -vt  | grep foo
-rw-r--r-- 0/0               6 2025-09-21 21:57 ./foo2
hrw-r--r-- 0/0               0 2025-09-21 21:57 ./foo link to ./foo2

This PR restricts hardlinking to files known to be hardlinks, that is, if nlink > 1 on the file stat.

In this example above,

  • /tmp/a/foo and /tmp/a/bar are hardlinks to the same inode
  • Only /tmp/a/foo is included in the package
  • /tmp/a/foo is included twice
  • because nlink>1 on this file, fpm will hardlink the second file to the first (foo2 to foo).

Solving this specific issue could be out of scope for #2102, so I think it's ok to leave unsolved.

@jordansissel

Copy link
Copy Markdown
Owner Author

The test suite is repeatedly failing on Github Actions due to apt-get install problems. The suite passes for me locally (with exception for two unrelated failures in perl/python tests)

Comment thread lib/fpm/command.rb
require "fpm/version"
require "fpm/util"
require "clamp"
require "ostruct"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this to get rid of a Ruby warning about a future change: "warning: ostruct was loaded from the standard library, but will no longer be part of the default gems starting from Ruby 3.5.0."

@t3chguy t3chguy left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

@jordansissel
jordansissel merged commit 1c3cfe7 into main Sep 27, 2025
0 of 6 checks passed
jordansissel added a commit that referenced this pull request Sep 30, 2025
Older rspec didn't allow a description to be given to satisfy(), but
newer ones do. I think I forgot to include this rspec version update
in PR #2103
jordansissel added a commit that referenced this pull request Sep 30, 2025
Older rspec didn't allow a description to be given to satisfy(), but
newer ones do. I think I forgot to include this rspec version update
in PR #2103
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants