twpayne/chezmoi

Add files within a symlinked directory

Closed this issue · 6 comments

What exactly are you trying to do?

I'm trying to manage some Firefox files present in the profile folder (eg. user.js, chrome/).

The profile directory (fcyz0js1.default-release-1684344024204 in my case) is a symlink, due the use of Profile Sync Daemon to keep profile in ram:

$ ls -l
fcyz0js1.default-release-1684344024204 -> /run/user/1000/psd/muttley-firefox-fcyz0js1.default-release-1684344024204

What have you tried so far?

I have tried with:

$ chezmoi add --follow fcyz0js1.default-release-1684344024204/user.js
chezmoi: lstat /home/muttley/.local/share/chezmoi/private_dot_mozilla/private_firefox/symlink_fcyz0js1.default-release-1684344024204/user.js: not a directory

but in chezmoi source directory I have only the file ~/.local/share/chezmoi/private_dot_mozilla/private_firefox/symlink_fcyz0js1.default-release-1684344024204 with inside the absolute path of the symlink target.

Where else have you checked for solutions?

I'm sure I have missed something, somewhere...

Output of any commands you've tried with --verbose flag

$ chezmoi --verbose add --follow fcyz0js1.default-release-1684344024204/user.js

Give the same result

Output of chezmoi doctor

$ chezmoi doctor
RESULT    CHECK                       MESSAGE
warning   version                     v2.47.4, built at 2024-04-13T09:22:15Z
ok        latest-version              v2.47.4
ok        os-arch                     linux/amd64 (Arch Linux)
ok        uname                       Linux thinkpad 6.8.7-arch1-1 #1 SMP PREEMPT_DYNAMIC Wed, 17 Apr 2024 15:20:28 +0000 x86_64 GNU/Linux
ok        go-version                  go1.22.2 (gc)
ok        executable                  /usr/bin/chezmoi
ok        config-file                 ~/.config/chezmoi/chezmoi.toml, last modified 2024-03-14T18:29:37+01:00
warning   source-dir                  ~/.local/share/chezmoi is a git working tree (dirty)
ok        suspicious-entries          no suspicious entries
warning   working-tree                ~/.local/share/chezmoi is a git working tree (dirty)
ok        dest-dir                    ~ is a directory
ok        umask                       022
ok        cd-command                  found /usr/bin/zsh
ok        cd-args                     /usr/bin/zsh
ok        diff-command                found /usr/bin/git
ok        edit-command                found /usr/bin/nvim
ok        edit-args                   /usr/bin/nvim
ok        git-command                 found /usr/bin/git, version 2.44.0
ok        merge-command               found /usr/bin/vimdiff
ok        shell-command               found /usr/bin/zsh
ok        shell-args                  /usr/bin/zsh
info      age-command                 age not found in $PATH
ok        gpg-command                 found /usr/bin/gpg, version 2.4.5
info      pinentry-command            not set
info      1password-command           op not found in $PATH
info      bitwarden-command           bw not found in $PATH
info      bitwarden-secrets-command   bws not found in $PATH
info      dashlane-command            dcli not found in $PATH
info      doppler-command             doppler not found in $PATH
info      gopass-command              gopass not found in $PATH
info      keepassxc-command           keepassxc-cli not found in $PATH
info      keepassxc-db                not set
info      keeper-command              keeper not found in $PATH
info      lastpass-command            lpass not found in $PATH
info      pass-command                pass not found in $PATH
info      passhole-command            ph not found in $PATH
info      rbw-command                 rbw not found in $PATH
info      vault-command               vault not found in $PATH
info      vlt-command                 vlt not found in $PATH
info      secret-command              not set

Additional context

In truth, the link should be two:

profile -> /home/muttley/.mozilla/firefox/fcyz0js1.default-release-1684344024204 -> /run/user/1000/psd/muttley-firefox-fcyz0js1.default-release-1684344024204

With this bad solution, I would like to sync many machines with different path for Firefox profile.

For the symlinks, chezmoi supports symlinks as templates (i.e. the target of the symlink is computed from a template), so you should be able to create the first two levels of symlinks with something like:

symlink_profile.tmpl:

{{ joinPath .chezmoi.homeDir ".mozilla" "firefox" "fcyz0js1.default-release-1684344024204" }}

dot_mozilla/firefox/symlink_fcyz0js1.default-release-1684344024204.tmpl:

{{ joinPath "run" "user" .chezmoi.uid "psd" (printf "%s-firefox-fcyz0js1.default-release-1684344024204" .chezmoi.username }}

However, the final files are in /run/user/1000, which is outside your home directory, so chezmoi becomes less helpful at this point. You could use this technique, or a script.

chezmoi also includes the mozillaInstallHash template function which might help with the symlink templates.

But in case I use symlink_ file with or without template extension, I need to point whole Firefox profile directory?

I need only to allow chezmoi to manage a directory and a file (inside profile):

  • /chrome/
  • /user.js

I think I should use this way: Handle different file locations on different systems with the same contents
But I have to create a symlin_*.tmpl file for each file in profile I want to manage, and make a copy of the original one in .chezmoitemplates/. It's quite complicated to maintain.

When I was thinking that chezmoi follow the symlink, I was written this script (run_once_before_install-firefox.sh):

#!/bin/bash

PKGS="firefox" 

if ! paru -Qi $PKGS > /dev/null 2>&1; then
    paru -S --skipreview $PKGS
fi

while true; do
    read -p "Enter the Firefox profile path (eg. '/home/user/.mozilla/firefox/wcv2zx27.default'): " dir_path
    if [ ! -d "$dir_path" ]; then
        echo "Invalid directory path. Please enter a valid directory!"
    else    
        break
    fi
done

# Get the parent directory path
parent_dir=$(dirname "$dir_path")

# Create a symbolic link to the profile directory
ln -s "${dir_path}" "$parent_dir/profile" 

# Print the symbolic link path
echo "The symbolic link path is: $parent_dir/profile"

That create the same dir symlink (/home/user/.mozilla/firefox/) in each pc, asking the right firefox profile directory to the user... There is a reason, why chezmoi doesn't follow fs symlink?

chezmoi also includes the mozillaInstallHash template function which might help with the symlink templates.

I have try to understand how and where to use this function, but I'm missing something ...

Thanks for your answer and detailed information!!

@elbowz By default, Firefox generates a profile with a random string (wcv2zx27 in your example) in its name on the first launch of each install type (Release, Dev, Nightly etc.). The name of the install type is typically used as the suffix of the profile name (XXXXXXXX.default, XXXXXXXX.default-release etc.).

Firefox stores these profile-to-install mappings in installs.ini and profiles.ini. When mapping a profile to an install, it uses a hash generated by Google's CityHash (version 1) of the install path as the key, e.g.:

installs.ini:

[308046B0AF4A39CB]
Default=Profiles/default-release
Locked=1

profiles.ini:

[BackgroundTasksProfiles]
MozillaBackgroundTask-308046B0AF4A39CB-backgroundupdate=q84q8f9w.MozillaBackgroundTask-308046B0AF4A39CB-backgroundupdate
MozillaBackgroundTask-308046B0AF4A39CB-defaultagent=h3bb4eyb.MozillaBackgroundTask-308046B0AF4A39CB-defaultagent

[General]
StartWithLastProfile=1
Version=2

[Install308046B0AF4A39CB]
Default=Profiles/default-release
Locked=1

[Profile0]
Default=1
IsRelative=1
Name=default-release
Path=Profiles/default-release

308046B0AF4A39CB is the hash for the path C:\Program Files\Mozilla Firefox. The INI files above set the default-release profile as the default profile for the install at C:\Program Files\Mozilla Firefox.

The mozillaInstallHash template function can be used to get the hash for a given install path, which can enable you to do the following:

  1. Create a custom named profile in your source state.
  2. Map the custom profile to a particular install of Firefox.
  3. Set this profile as the default profile and set StartWithLastProfile to 1 before even launching Firefox initially, meaning it does not create a new profile with a random name, and does not ask which profile you would like to use when it is launched.

On UNIX-like systems, you should be able to get the install path with something like which. On Windows, you likely need to read the registry, as the Firefox installer does not add the install path to your PATH environment variable. I use a helper script on Windows which returns the path from the registry.

Note that the toIni template function is not directly suitable for use in these INI files, as the formatting is different. I work around this by using a custom formatter template which I have in .chezmoitemplates/format/toMozillaIni:

{{- /*
Formats the pipeline to INI suitable for Mozilla *.ini files, e.g.:

[Profile1]
Name=default
IsRelative=1
Path=Profiles/eam0mjyl.default
Default=1

[Profile0]
Name=default-release
IsRelative=1
Path=Profiles/k4w6yu6v.default-release
*/ -}}

{{- range $section, $values := . }}
[{{ $section }}]
{{-   range $key, $value := $values }}
{{ $key }}={{ $value -}}
{{    end }}
{{ end }}

I create the INI sections in template variables, e.g.:

{{- $profiles := dict "Profile0" (dict
    "Name" "default-release"
    "Path" "Profiles/default-release"
    "IsRelative" 1
    "Default" 1
  )
-}}

Then pipe them to my custom formatter, e.g.:

{{- includeTemplate "format/toMozillaIni" $profiles | trim }}

I made an example repo a while ago, then purged it following some new (at the time) chezmoi features which made it massively easier to manage Firefox profiles, with the intention of recreating it, but I never managed to get round to it. I suppose now is a good time.

It might not seem like it, but it's a lot easier to manage the entire profile with chezmoi and only add the files you need (user.js etc.) than it is to put the files into an existing profile directory, as you would likely need to parse installs.ini and/or profiles.ini at a minimum to determine the correct profile.

Profile-sync-daemon seems to also add a snapshot suffix, which complicates this even further. Generally, with chezmoi, you want the names of things to be static/non-random wherever possible.

Ciao @bradenhilton, thank you very much for your deeply detailed information!

What I had missed about the mozillaInstallHash chezmoi utility, was the path arguments. Maybe something like:
Firefox installation path (eg. /usr/lib/firefox) in the documentation could be useful.

I don't know if I well understood what I have to do, to keep in sync between machines some files in the Firefox profile directory.
I'm thinking to create two template files:

profiles.ini.tmpl

{{ $mozillaHash = mozillaInstallHash "/usr/lib/firefox" }}

[Install{{- $mozillaHash -}}]
Default=default-release
Locked=1

[Profile0]
Name=default
IsRelative=1
Path=default-release
Default=1

[General]
StartWithLastProfile=1
Version=2

installs.ini.tmpl

{{ $mozillaHash = mozillaInstallHash "/usr/lib/firefox" }}

[{{- $mozillaHash -}}]
Default=wcv2zx27.default

Put both files in {{ .chezmoi.sourceDir }}/dot_mozilla/dot_firefox/.

After that, when start Firefox I should have a new default profile folder called default-release, and there I can manage which file I want with chezmoi, right?!

I made an example repo a while ago, then purged it following some new (at the time) chezmoi features which made it massively easier to manage Firefox profiles, with the intention of recreating it, but I never managed to get round to it. I suppose now is a good time.

Please, yesss :)

It might not seem like it, but it's a lot easier to manage the entire profile with chezmoi and only add the files you need (user.js etc.) than it is to put the files into an existing profile directory, as you would likely need to parse installs.ini and/or profiles.ini at a minimum to determine the correct profile.

This is not so clear, what do you mean?

Profile-sync-daemon seems to also add a snapshot suffix, which complicates this even further. Generally, with chezmoi, you want the names of things to be static/non-random wherever possible.

Yes, In the PSD path used for store profile (/run/user/1000/psd/muttley-firefox-fcyz0js1.default-release-1684344024204) the last part 1684344024204 I don't know how is generated...for this reason I think (but I can wrong), add an option like rsync -L/--copy-links could solve a lot of these issues.

Your installs.ini.tmpl isn't quite right, it should have the same profile path as the profile in profiles.ini.tmpl (default-release in this case, because it's a relative path).

See http://kb.mozillazine.org/Profiles.ini_file if you need more info on the options.

Be sure to verify that the correct install path is being used with mozillaInstallHash by running

chezmoi execute-template '{{ mozillaInstallHash "<PATH>" }}'

and compare the hash output with your existing INI files. I know that some versions of Firefox are installed in /opt, for example.

Put both files in {{ .chezmoi.sourceDir }}/dot_mozilla/dot_firefox/.

After that, when start Firefox I should have a new default profile folder called default-release, and there I can manage which file I want with chezmoi, right?!

You should be able to mostly reuse your existing profile, explained below. Note that it should be firefox instead of dot_firefox.

It might not seem like it, but it's a lot easier to manage the entire profile with chezmoi and only add the files you need (user.js etc.) than it is to put the files into an existing profile directory, as you would likely need to parse installs.ini and/or profiles.ini at a minimum to determine the correct profile.

This is not so clear, what do you mean?

You can create the profile directory in your source state and use templates in it. For a profile on Linux with a relative path, you would want to create the directory {{ .chezmoi.sourceDir }}/dot_mozilla/firefox/default-release.

You can start this completely from scratch, which will require setting up things like add-ons, setting login cookies etc.

If you don't want to do that, you should just be able to do something like:

mv ~/.mozilla/firefox/wcv2zx27.default ~/.mozilla/firefox/default-release

and preserve most of your existing profile at least.

I don't have a Linux machine set up yet, so the above is untested. You might prefer to just copy the profile instead of renaming it to be safe. This will also likely interfere with Profile-sync-daemon, so you might want to disable it while experimenting.

Once you have your existing profile at ~/.mozilla/firefox/default-release, and you have the INI files and profile set up in {{ .chezmoi.sourceDir }}/dot_mozilla/firefox/, you can start adding files to {{ .chezmoi.sourceDir }}/dot_mozilla/firefox/default-release, such as user.js.

I've added my current setup below. It uses modify templates (one of the new [at the time] chezmoi features I was talking about in my previous response), so it's a little more complex than basic templates, but the concept remains the same. I intend for it to work on more than one platform, but currently I only have the Windows side setup correctly. You should be able to modify the files to fit your needs though.

Firefox

I use modify templates because Firefox likes to create background tasks to aid with automatic updates etc., at least on Windows. Like non-custom profiles, these also contain a random string. You can see them in the [BackgroundTasksProfiles] section in my previous response. If this does not happen on Linux, you can just use a regular template.

The modify template approach also allows me to minimize diffs, because I parse the existing file contents from .chezmoi.stdin to two different template variables, one of which I then modify. At the end, I compare the two variables. If they are the same, I just print out .chezmoi.stdin. If they differ, I format the modified data appropriately and print that.

All paths below are relative to the source directory (default ~/.local/share/chezmoi).

.chezmoitemplates/firefox/modify_installs.ini

This file does not have multi-platform support at the moment. Again, you can just edit it to suit your needs.

{{- $installPath := "" -}}
{{- if eq .chezmoi.os "windows" -}}
{{-   $installPath = default "C:\\Program Files\\Mozilla Firefox" (output "powershell.exe" "-File" (joinPath .chezmoi.sourceDir "private_dot_local" "bin" "Get-InstallLocation.ps1") "-ProgramName" "Mozilla Firefox" | trim) -}}
{{- end -}}

{{- $installHash := mozillaInstallHash $installPath -}}
{{- $profileName := "default-release" -}}

{{- $currentInstalls := .chezmoi.stdin | fromIni -}}
{{- $modifiedInstalls := .chezmoi.stdin | fromIni -}}

{{- /* Set default profile here */ -}}
{{- $_ := set $modifiedInstalls 
  $installHash (dict 
    "Default" (printf "Profiles/%s" $profileName)
    "Locked" 1
  )
-}}

{{- if deepEqual $currentInstalls $modifiedInstalls -}}
{{-   .chezmoi.stdin }}
{{- else -}}
{{-   includeTemplate "format/toMozillaIni" $modifiedInstalls | trim }}
{{- end }}

.chezmoitemplates/firefox/modify_profiles.ini

This file also does not have full multi-platform support at the moment. One of the steps involves removing all profile sections from the data, then only adding the one(s) I care about. This doesn't actually delete the profiles from disk.

{{- $currentProfiles := .chezmoi.stdin | fromIni -}}
{{- $modifiedProfiles := .chezmoi.stdin | fromIni -}}

{{- $profileName := "default-release" -}}

{{- $installPath := "" -}}
{{- $installHash := "" -}}
{{- $profilePath := "" -}}

{{- if eq .chezmoi.os "darwin" -}}
{{-   $installPath = "" -}}
{{-   $installHash = mozillaInstallHash $installPath -}}
{{-   $profilePath = $profileName -}}
{{- else if eq .chezmoi.os "linux" -}}
{{-   $installPath = "" -}}
{{-   $installHash = mozillaInstallHash $installPath -}}
{{-   $profilePath = $profileName -}}
{{- else if eq .chezmoi.os "windows" -}}
{{-   $installPath = default "C:\\Program Files\\Mozilla Firefox" (output "powershell.exe" "-File" (joinPath .chezmoi.sourceDir "private_dot_local" "bin" "Get-InstallLocation.ps1") "-ProgramName" "Mozilla Firefox" | trim) -}}
{{-   $installHash = mozillaInstallHash $installPath -}}
{{-   $profilePath = printf "Profiles/%s" $profileName -}}
{{- end -}}

{{- /* Set default profile here */ -}}
{{- $_ := set $modifiedProfiles
  (printf "Install%s" $installHash) (dict
    "Default" $profilePath
    "Locked" 1
  )
-}}

{{- /* Remove existing profiles if present */ -}}
{{- range $section, $values := $modifiedProfiles -}}
{{-   if regexMatch "^Profile\\d+$" $section -}}
{{-     $_ := unset $modifiedProfiles $section -}}
{{-   end -}}
{{- end -}}

{{- /* Add profiles here */ -}}
{{- $_ := set $modifiedProfiles
  "Profile0" (dict
    "Name" $profileName
    "Path" $profilePath
    "IsRelative" 1
    "Default" 1
  )
-}}

{{- $_ := set $modifiedProfiles
  "General" (dict
    "StartWithLastProfile" 1
    "Version" 2
  )
-}}

{{- if deepEqual $currentProfiles $modifiedProfiles -}}
{{-   .chezmoi.stdin }}
{{- else -}}
{{-   includeTemplate "format/toMozillaIni" $modifiedProfiles | trim }}
{{- end }}

.chezmoitemplates/firefox/user.js

This template has a custom left-delimiter to allow me to use JS formatters/linters etc.

// chezmoi:template:left-delimiter=//{{
user_pref('browser.aboutConfig.showWarning', false);

// Downloads
//{{ if eq .chezmoi.os "windows" -}}
user_pref('browser.download.lastDir', '%USERPROFILE%\\Downloads');
//{{- else -}}
user_pref('browser.download.lastDir', '$HOME/Downloads');
//{{- end }}
user_pref('browser.download.lastDir.savePerSite', false);

// General preferences
user_pref('browser.toolbars.bookmarks.visibility', 'always');
user_pref('findbar.highlightAll', true);
user_pref('general.autoScroll', true);
user_pref('media.eme.enabled', true);
user_pref('font.name.monospace.x-western', 'Fira Code');

// Enable userChrome.css
user_pref('toolkit.legacyUserProfileCustomization.stylesheets', true);

// Custom sync
user_pref('services.sync.prefs.sync.browser.uiCustomization.state', true);

.chezmoitemplates/format/toMozillaIni

{{- /*
Formats the pipeline to INI suitable for Mozilla *.ini files, e.g.:

[Profile1]
Name=default
IsRelative=1
Path=Profiles/eam0mjyl.default
Default=1

[Profile0]
Name=default-release
IsRelative=1
Path=Profiles/k4w6yu6v.default-release
*/ -}}

{{- range $section, $values := . }}
[{{ $section }}]
{{-   range $key, $value := $values }}
{{ $key }}={{ $value -}}
{{    end }}
{{ end }}

AppData/Roaming/Mozilla/Firefox/modify_installs.ini

; chezmoi:modify-template
; chezmoi:template:left-delimiter=;{{
;{{- includeTemplate "firefox/modify_installs.ini" . }}

AppData/Roaming/Mozilla/Firefox/modify_profiles.ini

; chezmoi:modify-template
; chezmoi:template:left-delimiter=;{{
;{{- includeTemplate "firefox/modify_profiles.ini" . }}

AppData/Roaming/Mozilla/Firefox/Profiles/default-release/user.js

{{ includeTemplate "firefox/user.js" . | trim }}

If you only care about a single platform, you don't need the .chezmoitemplates/firefox/* files, and can instead create them directly in AppData/Roaming/Mozilla/Firefox or dot_mozilla/firefox, etc.

This answer is more than what I expected, or I wished...It's a very detailed tutorial!

Thank you very much!!

edit: If I cannot use it with the Profile Sync Daemon (ie. unpredictable path in /run/user/) , I think to disable it at the end...