Repository: gramps-project/gramps
Pull request #2013: Simplify logic for setting data, config and cache directories
Author: login: Nick-Hall
Labels: name: bug
Created: 2025-03-12T23:21:27Z | Merged: 2025-03-15T18:06:32Z

This PR is illustrates a simplification of the logic for setting the data, config and cache directories.

* The old `$HOME/.gramps` location for Linux is removed in favour of the newer XDG specification.
* For Windows `$APPDATA` is used with a fallback to `$USERPROFILE/AppData/Roaming`.

The old locations can still be used by setting the `$GRAMPSHOME` variable.

If we decide to adopt this approach then we should consider migrating existing data.

Fixes [#13261](https://gramps-project.org/bugs/view.php?id=13261), [#13300](https://gramps-project.org/bugs/view.php?id=13300), [#13686](https://gramps-project.org/bugs/view.php?id=13686).


Diff:
diff --git a/.github/workflows/gramps-ci.yml b/.github/workflows/gramps-ci.yml
index 570d994b124..d8c06d35711 100644
--- a/.github/workflows/gramps-ci.yml
+++ b/.github/workflows/gramps-ci.yml
@@ -59,11 +59,11 @@ jobs:
         python3 -m pip install orjson
     - name: Install addons
       run: |
-        mkdir -p ~/.gramps/gramps60/plugins/
+        mkdir -p ~/.local/share/gramps/gramps60/plugins/
         wget https://github.com/gramps-project/addons/raw/master/gramps60/download/CliMerge.addon.tgz
-        tar -C ~/.gramps/gramps60/plugins -xzf CliMerge.addon.tgz
+        tar -C ~/.local/share/gramps/gramps60/plugins/ -xzf CliMerge.addon.tgz
         wget https://github.com/gramps-project/addons/raw/master/gramps60/download/ExportRaw.addon.tgz
-        tar -C ~/.gramps/gramps60/plugins -xzf ExportRaw.addon.tgz
+        tar -C ~/.local/share/gramps/gramps60/plugins/ -xzf ExportRaw.addon.tgz
     - name: Build
       run: |
         python3 setup.py build
diff --git a/gramps/gen/config.py b/gramps/gen/config.py
index 02cee872899..33f4f24e3d2 100644
--- a/gramps/gen/config.py
+++ b/gramps/gen/config.py
@@ -391,11 +391,24 @@ def emit(key):
 if __debug__:  # enable a simple CLI test to see if the datestrings exist
     register("test.january", _("January", "localized lexeme inflections"))
 
+
 # ---------------------------------------------------------------
 #
 # Upgrade Conversions go here.
 #
 # ---------------------------------------------------------------
+def load_previous(ini_file):
+    """
+    Attempt to load a `gramps.ini` file from an earlier version.
+    Return True if the file exists.
+    """
+    if os.path.exists(ini_file):
+        logging.info("Importing old config file '%s'...", ini_file)
+        CONFIGMAN.load(ini_file)
+        logging.info("Done importing old config file '%s'", ini_file)
+        return True
+    return False
+
 
 # If we have not already upgraded to this version,
 # we can tell by seeing if there is a key file for this version:
@@ -410,6 +423,7 @@ def emit(key):
     # check previous version of gramps:
     fullpath, filename = os.path.split(CONFIGMAN.filename)
     fullpath, previous = os.path.split(fullpath)
+    oldpath = os.path.join(USER_HOME, ".gramps")
     match = re.match(r"gramps(\d*)", previous)
     if match:
         # cycle back looking for previous versions of gramps
@@ -422,11 +436,11 @@ def emit(key):
             # Perhaps addings specific list of versions to check
             # -----------------------------------------
             digits = str(int(match.groups()[0]) - i)
-            previous_grampsini = os.path.join(fullpath, "gramps" + digits, filename)
-            if os.path.exists(previous_grampsini):
-                logging.info("Importing old config file '%s'...", previous_grampsini)
-                CONFIGMAN.load(previous_grampsini)
-                logging.info("Done importing old config file '%s'", previous_grampsini)
+            grampsini = os.path.join(fullpath, "gramps" + digits, filename)
+            if load_previous(grampsini):
+                break
+            grampsini = os.path.join(oldpath, "gramps" + digits, filename)
+            if load_previous(grampsini):
                 break
 
 # ---------------------------------------------------------------
diff --git a/gramps/gen/const.py b/gramps/gen/const.py
index 7651dca427a..3ee3dbff6ae 100644
--- a/gramps/gen/const.py
+++ b/gramps/gen/const.py
@@ -34,6 +34,10 @@
 import random
 import sys
 import uuid
+import shutil
+import logging
+
+LOG = logging.getLogger(".")
 
 from gi.repository import GLib
 
@@ -90,47 +94,49 @@
 
 # -------------------------------------------------------------------------
 #
-# Determine the home directory. According to Wikipedia, most UNIX like
-# systems use HOME. I'm assuming that this would apply to OS X as well.
-# Windows apparently uses USERPROFILE
+# Determine the user data and user configuration directories.
 #
 # -------------------------------------------------------------------------
 if "GRAMPSHOME" in os.environ:
     USER_HOME = get_env_var("GRAMPSHOME")
-    HOME_DIR = os.path.join(USER_HOME, "gramps")
+    USER_DATA = os.path.join(USER_HOME, "gramps")
+    USER_CONFIG = USER_DATA
 elif "USERPROFILE" in os.environ:
     USER_HOME = get_env_var("USERPROFILE")
-    HOME_DIR = os.path.join(GLib.get_user_config_dir(), "gramps")
-    if not os.path.exists(HOME_DIR):
-        homedir = get_env_var("APPDATA") if "APPDATA" in os.environ else USER_HOME
-        homedir = os.path.join(homedir, "gramps")
-        dbpath = os.path.join(homedir, "grampsdb")
-        if os.path.isdir(dbpath) and os.listdir(dbpath):
-            HOME_DIR = homedir
-else:
-    USER_HOME = get_env_var("HOME")
-    HOME_DIR = os.path.join(USER_HOME, ".gramps")
-ORIG_HOME_DIR = HOME_DIR
-if "SAFEMODE" in os.environ:
-    if "USERPROFILE" in os.environ:
-        USER_HOME = get_env_var("USERPROFILE")
+    if "APPDATA" in os.environ:
+        USER_DATA = os.path.join(get_env_var("APPDATA"), "gramps")
     else:
-        USER_HOME = get_env_var("HOME")
-    HOME_DIR = get_env_var("SAFEMODE")
-
-
-if os.path.exists(HOME_DIR) or "GRAMPSHOME" in os.environ or "SAFEMODE" in os.environ:
-    USER_DATA = HOME_DIR
-    USER_CONFIG = HOME_DIR
-    USER_CACHE = HOME_DIR
+        USER_DATA = os.path.join(USER_HOME, "AppData", "Roaming", "gramps")
+    USER_CONFIG = USER_DATA
+    # Migrate data from AppData\Local to AppData\Roaming on Windows.
+    OLD_HOME = os.path.join(USER_HOME, "AppData", "Local", "gramps")
+    if os.path.exists(OLD_HOME):
+        if os.path.exists(USER_DATA):
+            LOG.warning("Two Gramps application data directories exist.")
+        else:
+            shutil.move(OLD_HOME, USER_DATA)
 else:
+    USER_HOME = get_env_var("HOME")
     USER_DATA = os.path.join(GLib.get_user_data_dir(), "gramps")
     USER_CONFIG = os.path.join(GLib.get_user_config_dir(), "gramps")
-    USER_CACHE = os.path.join(GLib.get_user_cache_dir(), "gramps")
+    # Copy the database directory into the XDG directory.
+    OLD_HOME = os.path.join(USER_HOME, ".gramps")
+    if os.path.exists(OLD_HOME):
+        if os.path.exists(USER_DATA) or os.path.exists(USER_CONFIG):
+            LOG.warning("Two Gramps application data directories exist.")
+        else:
+            db_dir = os.path.join(OLD_HOME, "grampsdb")
+            if os.path.exists(db_dir):
+                shutil.copytree(db_dir, os.path.join(USER_DATA, "grampsdb"))
+
+USER_CACHE = os.path.join(GLib.get_user_cache_dir(), "gramps")
+
+if "SAFEMODE" in os.environ:
+    USER_CONFIG = get_env_var("SAFEMODE")
 
 USER_PICTURES = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES)
 if not USER_PICTURES:
-    USER_PICTURES = HOME_DIR
+    USER_PICTURES = USER_DATA
 
 VERSION_DIR_NAME = "gramps%s%s" % (VERSION_TUPLE[0], VERSION_TUPLE[1])
 VERSION_DIR = os.path.join(USER_CONFIG, VERSION_DIR_NAME)
diff --git a/gramps/grampsapp.py b/gramps/grampsapp.py
index 598c6aade83..28777d64ea4 100644
--- a/gramps/grampsapp.py
+++ b/gramps/grampsapp.py
@@ -51,7 +51,7 @@
 # Gramps modules
 #
 # -------------------------------------------------------------------------
-from .gen.const import APP_GRAMPS, USER_DIRLIST, USER_DATA, ORIG_HOME_DIR
+from .gen.const import APP_GRAMPS, USER_DIRLIST, USER_DATA
 from .gen.constfunc import mac
 from .version import VERSION_TUPLE
 from .gen.constfunc import win, get_env_var
@@ -654,11 +654,6 @@ def run():
     argv_copy = sys.argv[:]
     argpars = ArgParser(argv_copy)
 
-    # if in safe mode we should point the db dir back to the original dir.
-    # It is ok to import config here, 'Defaults' command had its chance...
-    if "SAFEMODE" in os.environ:
-        config.set("database.path", os.path.join(ORIG_HOME_DIR, "grampsdb"))
-
     # On windows the fontconfig handler is a better choice
     if win() and ("PANGOCAIRO_BACKEND" not in os.environ):
         os.environ["PANGOCAIRO_BACKEND"] = "fontconfig"
diff --git a/gramps/plugins/test/imports_test.py b/gramps/plugins/test/imports_test.py
index 3e83e008fc8..9078677af5b 100644
--- a/gramps/plugins/test/imports_test.py
+++ b/gramps/plugins/test/imports_test.py
@@ -46,7 +46,7 @@
 from gramps.gen.simple import SimpleAccess
 from gramps.gen.utils.id import set_det_id
 from gramps.gen.user import User
-from gramps.gen.const import HOME_DIR, DATA_DIR
+from gramps.gen.const import USER_DATA, DATA_DIR
 from gramps.test.test_util import capture
 from gramps.plugins.export.exportxml import XmlWriter
 from gramps.gen.db.dbconst import DBBACKEND
@@ -65,7 +65,7 @@
 # the following defines where to find the test import and result files
 TEST_DIR = os.path.abspath(os.path.join(DATA_DIR, "tests"))
 # the following defines where to find test error diffs and import result XML files
-TEMP_DIR = os.path.join(HOME_DIR, "temp")
+TEMP_DIR = os.path.join(USER_DATA, "temp")
 if not os.path.isdir(TEMP_DIR):
     os.makedirs(TEMP_DIR)
 


Reviews:
user: login: jralls
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T17:33:07Z
user: login: Nick-Hall
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T17:39:42Z
user: login: jralls
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T18:04:48Z
user: login: Nick-Hall
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T18:36:31Z
user: login: Nick-Hall
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T19:25:32Z
user: login: jralls
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-13T19:36:48Z
user: login: Nick-Hall
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T19:36:55Z

Review comments:
diff_hunk: @@ -104,10 +105,20 @@
     else:
         USER_DATA = os.path.join(USER_HOME, "AppData", "Roaming", "gramps")
     USER_CONFIG = USER_DATA
+    # Migrate data from AppData\Local to AppData\Roaming on Windows.
+    OLD_HOME = os.path.join(USER_HOME, "AppData", "Local", "gramps")
+    if os.path.exists(OLD_HOME) and not os.path.exists(USER_DATA):
+        shutil.move(OLD_HOME, USER_DATA)
path: gramps/gen/const.py
user: login: jralls
body: We should at least log a warning if both exist.
created_at: 2025-03-13T17:30:48Z
author_association: MEMBER
diff_hunk: @@ -104,10 +105,20 @@
     else:
         USER_DATA = os.path.join(USER_HOME, "AppData", "Roaming", "gramps")
     USER_CONFIG = USER_DATA
+    # Migrate data from AppData\Local to AppData\Roaming on Windows.
+    OLD_HOME = os.path.join(USER_HOME, "AppData", "Local", "gramps")
+    if os.path.exists(OLD_HOME) and not os.path.exists(USER_DATA):
+        shutil.move(OLD_HOME, USER_DATA)
 else:
     USER_HOME = get_env_var("HOME")
     USER_DATA = os.path.join(GLib.get_user_data_dir(), "gramps")
     USER_CONFIG = os.path.join(GLib.get_user_config_dir(), "gramps")
+    # Copy database directory into the XDG data directory.
+    OLD_HOME = os.path.join(USER_HOME, ".gramps")
+    if os.path.exists(OLD_HOME) and not os.path.exists(USER_DATA):
+        shutil.copytree(
+            os.path.join(OLD_HOME, "grampsdb"), os.path.join(USER_DATA, "grampsdb")
+        )
path: gramps/gen/const.py
user: login: jralls
body: What about USER_CONFIG?
created_at: 2025-03-13T17:33:01Z
author_association: MEMBER
diff_hunk: @@ -104,10 +105,20 @@
     else:
         USER_DATA = os.path.join(USER_HOME, "AppData", "Roaming", "gramps")
     USER_CONFIG = USER_DATA
+    # Migrate data from AppData\Local to AppData\Roaming on Windows.
+    OLD_HOME = os.path.join(USER_HOME, "AppData", "Local", "gramps")
+    if os.path.exists(OLD_HOME) and not os.path.exists(USER_DATA):
+        shutil.move(OLD_HOME, USER_DATA)
 else:
     USER_HOME = get_env_var("HOME")
     USER_DATA = os.path.join(GLib.get_user_data_dir(), "gramps")
     USER_CONFIG = os.path.join(GLib.get_user_config_dir(), "gramps")
+    # Copy database directory into the XDG data directory.
+    OLD_HOME = os.path.join(USER_HOME, ".gramps")
+    if os.path.exists(OLD_HOME) and not os.path.exists(USER_DATA):
+        shutil.copytree(
+            os.path.join(OLD_HOME, "grampsdb"), os.path.join(USER_DATA, "grampsdb")
+        )
path: gramps/gen/const.py
user: login: Nick-Hall
body: We probably only need the `gramps.ini` file.

I also need to check that the `grampsdb` directory exists before attempting to copy it.
created_at: 2025-03-13T17:39:42Z
author_association: MEMBER
diff_hunk: @@ -104,10 +105,20 @@
     else:
         USER_DATA = os.path.join(USER_HOME, "AppData", "Roaming", "gramps")
     USER_CONFIG = USER_DATA
+    # Migrate data from AppData\Local to AppData\Roaming on Windows.
+    OLD_HOME = os.path.join(USER_HOME, "AppData", "Local", "gramps")
+    if os.path.exists(OLD_HOME) and not os.path.exists(USER_DATA):
+        shutil.move(OLD_HOME, USER_DATA)
 else:
     USER_HOME = get_env_var("HOME")
     USER_DATA = os.path.join(GLib.get_user_data_dir(), "gramps")
     USER_CONFIG = os.path.join(GLib.get_user_config_dir(), "gramps")
+    # Copy database directory into the XDG data directory.
+    OLD_HOME = os.path.join(USER_HOME, ".gramps")
+    if os.path.exists(OLD_HOME) and not os.path.exists(USER_DATA):
+        shutil.copytree(
+            os.path.join(OLD_HOME, "grampsdb"), os.path.join(USER_DATA, "grampsdb")
+        )
path: gramps/gen/const.py
user: login: jralls
body: > We probably only need the gramps.ini file.

I think we just need for [config.py](https://github.com/gramps-project/gramps/blob/master/gramps/gen/config.py#L410) to check the old location.
created_at: 2025-03-13T18:04:48Z
author_association: MEMBER
diff_hunk: @@ -104,10 +105,20 @@
     else:
         USER_DATA = os.path.join(USER_HOME, "AppData", "Roaming", "gramps")
     USER_CONFIG = USER_DATA
+    # Migrate data from AppData\Local to AppData\Roaming on Windows.
+    OLD_HOME = os.path.join(USER_HOME, "AppData", "Local", "gramps")
+    if os.path.exists(OLD_HOME) and not os.path.exists(USER_DATA):
+        shutil.move(OLD_HOME, USER_DATA)
 else:
     USER_HOME = get_env_var("HOME")
     USER_DATA = os.path.join(GLib.get_user_data_dir(), "gramps")
     USER_CONFIG = os.path.join(GLib.get_user_config_dir(), "gramps")
+    # Copy database directory into the XDG data directory.
+    OLD_HOME = os.path.join(USER_HOME, ".gramps")
+    if os.path.exists(OLD_HOME) and not os.path.exists(USER_DATA):
+        shutil.copytree(
+            os.path.join(OLD_HOME, "grampsdb"), os.path.join(USER_DATA, "grampsdb")
+        )
path: gramps/gen/const.py
user: login: Nick-Hall
body: I'll look into it.  Using existing code and the same upgrade path would be a better option.
created_at: 2025-03-13T18:36:31Z
author_association: MEMBER
diff_hunk: @@ -104,10 +105,20 @@
     else:
         USER_DATA = os.path.join(USER_HOME, "AppData", "Roaming", "gramps")
     USER_CONFIG = USER_DATA
+    # Migrate data from AppData\Local to AppData\Roaming on Windows.
+    OLD_HOME = os.path.join(USER_HOME, "AppData", "Local", "gramps")
+    if os.path.exists(OLD_HOME) and not os.path.exists(USER_DATA):
+        shutil.move(OLD_HOME, USER_DATA)
path: gramps/gen/const.py
user: login: Nick-Hall
body: OK. I've added a warning.
created_at: 2025-03-13T19:25:32Z
author_association: MEMBER
diff_hunk: @@ -104,10 +105,20 @@
     else:
         USER_DATA = os.path.join(USER_HOME, "AppData", "Roaming", "gramps")
     USER_CONFIG = USER_DATA
+    # Migrate data from AppData\Local to AppData\Roaming on Windows.
+    OLD_HOME = os.path.join(USER_HOME, "AppData", "Local", "gramps")
+    if os.path.exists(OLD_HOME) and not os.path.exists(USER_DATA):
+        shutil.move(OLD_HOME, USER_DATA)
 else:
     USER_HOME = get_env_var("HOME")
     USER_DATA = os.path.join(GLib.get_user_data_dir(), "gramps")
     USER_CONFIG = os.path.join(GLib.get_user_config_dir(), "gramps")
+    # Copy database directory into the XDG data directory.
+    OLD_HOME = os.path.join(USER_HOME, ".gramps")
+    if os.path.exists(OLD_HOME) and not os.path.exists(USER_DATA):
+        shutil.copytree(
+            os.path.join(OLD_HOME, "grampsdb"), os.path.join(USER_DATA, "grampsdb")
+        )
path: gramps/gen/const.py
user: login: Nick-Hall
body: I've added a check for the old path to the config update code.
created_at: 2025-03-13T19:36:55Z
author_association: MEMBER

PR comments:
user: login: emyoulation
created_at: 2025-03-13T00:11:44Z
body: > - For Windows `$APPDATA` is used with a fallback to `$USERPROFILE/AppData/Roaming`.

Please double-check this.  At the same time Gramps switched to XDG, the new installs changed to `$LOCALAPPDATA` (aka `%LocalAppData%` ) 
The path for upgrades remained at `$APPDATA`  (aka `%AppData%` )

Frankly, the 2 possible locations is making troubleshooting a Windows issue much, much harder. 

Is it reasonable to put in a 6.1 enhancement request to move the `gramps` folder contents in the old location to the new one during 6.1 installation?  Or to just make 6.1 complain about a `gramps` folder in `%AppData%` until the user resolves it? 
author_association: CONTRIBUTOR
user: login: Nick-Hall
created_at: 2025-03-13T00:19:45Z
body: > Frankly, the 2 possible locations is making troubleshooting a Windows issue much, much harder.

I agree.  That's why I would like to use a single location for Windows - `AppData/Roaming` or `AppData/Local` - I don't mind which.  It would also be simpler if we removed the old `~/.gramps` location for Linux.
author_association: MEMBER
user: login: emyoulation
created_at: 2025-03-13T00:39:11Z
body: There were major arguments about the 2 locations when the shift was implemented. I was against changing in the middle of the 5.x family.  It seemed like a 6.x change.  

But everyone else insisted that the `%LocalAppData%` was the only possible option.  So let's not fight that battle again and go with `%LocalAppData%`

I only have an uneducated opinion on Linux file locations.  So I will not offer any Linux preference.
author_association: CONTRIBUTOR
user: login: Nick-Hall
created_at: 2025-03-13T00:49:42Z
body: > So let's not fight that battle again and go with %LocalAppData%

I have no objection to using `%LocalAppData%`.  However it seems sensible to use `%AppData%` if set.  I'm not a Windows user though, so I'm looking for feedback.
author_association: MEMBER
user: login: emyoulation
created_at: 2025-03-13T01:46:14Z
body: I'd be (and was!) for `%AppData%` if this was before the XDG switchover. (It seemed like the benefit of switching to `%LocalAppData%` was not worth the training and support costs.)

But now that the switch is started, aborting and reverting back to `%AppData%` would be saying there was bad planning. I'm distinctly opposed to making that statement when there was so much hard-fought consensus. (Even when I was on the out-voted side.)
author_association: CONTRIBUTOR
user: login: jralls
created_at: 2025-03-13T03:23:02Z
body: > But now that the switch is started, aborting and reverting back to %AppData% would be saying there was bad planning. I'm distinctly opposed to making that statement when there was so much hard-fought consensus. (Even when I was on the out-voted side.)

If [this](https://gramps.discourse.group/t/aio-move-of-user-directory-from-appdata-to-localappdata/4156) is the discussion you're referring to it doesn't look to me like the consensus was that it wouldn't make much practical difference between using LocalAppData and Appdata and that grampsdb really belongs in Documents. That discussion happened *after* #1368 was merged so it was more like no planning rather than bad planning.

We also have almost 20 years of using Roaming and only a little over 2 of using Local. A lot more users have there data in Roaming; only those who started using Gramps with 5.2 will have data in Local. 

In order to not freak out those users whose files are in Local we need to look there for their previous config and data. I propose that if we find it we simply move (rename on Windows) it to Roaming. Gramps 5.2 will find it there and be happy should the user go back and forth for comparison.

author_association: MEMBER
user: login: emyoulation
created_at: 2025-03-13T04:07:00Z
body: You also have the possibility that Gramps exists in both locations. So a simple rename may not always work.

Whatever is decided, so long as it goes back to a single possible path for the Gramps User Directory, I think everyone will be happier. (And the wiki will be simplified. And the Discourse troubleshooting instructions will be less convoluted.)


author_association: CONTRIBUTOR
user: login: jralls
created_at: 2025-03-13T04:54:27Z
body: > You also have the possibility that Gramps exists in both locations. So a simple rename may not always work.

That's [#13300](https://gramps-project.org/bugs/view.php?id=13300). Arguably rare enough that we could just bail out and tell the user to sort it out and then restart Gramps.

Or a middle route: If Local\gramps\gramps52 exists and Roaming\gramps\gramps52 doesn't and there aren't any common directory names in the two grampsdb directories then we can go ahead and do the move. 

author_association: MEMBER
user: login: emyoulation
created_at: 2025-03-13T08:52:36Z
body: Has anyone besides me suddenly started getting odd GitHub thread comment notifications? 

Where they have duplicated the content... 1st with a standard email header then with a thread  comment notification header:
![Screenshot_20250313_034950_Yahoo Mail.jpg](https://github.com/user-attachments/assets/0ffcf58d-b0df-4d69-ad54-2be846be008d)

This started recently.

Who do we contact at GitHub? It doubles their message sizes and archiving burden.
author_association: CONTRIBUTOR
user: login: jralls
created_at: 2025-03-13T16:21:37Z
body: >  Has anyone besides me suddenly started getting odd GitHub thread comment notifications?

Github sends out a mime-multipart with a plain-text part and an html part. This is pretty standard nowadays, most likely your mail goes out that way too unless you have html mail turned off. What might have changed is that your email client is now showing you both parts. You can look at the raw source of the email to see if the message is really duplicated within one of the parts. The declaration that it's multpart looks like
```
Content-Type: multipart/alternative;
 boundary="--==_mimepart_67d29cee64a12_7fde0918d1";
 charset=UTF-8
Content-Transfer-Encoding: 7bit
```
There are some more headers after that. The text part begins with
```
----==_mimepart_67d29cee64a12_7fde0918d1
Content-Type: text/plain;
 charset=UTF-8
Content-Transfer-Encoding: 7bit
```
and the html part begins with
```
Message ID: <gramps-project/gramps/pull/2013/c2720456566@github.com>
----==_mimepart_67d29cee64a12_7fde0918d1
Content-Type: text/html;
 charset=UTF-8
Content-Transfer-Encoding: quoted-printable
```
and the message ends with
```
----==_mimepart_67d29cee64a12_7fde0918d1--
```
> Who do we contact at GitHub?

https://support.github.com/contact/bug-report

But it's not likely to be their problem.
author_association: MEMBER
user: login: emyoulation
created_at: 2025-03-13T16:34:25Z
body: Thanks. That means the text/html redundancy is probably another of Yahoo's behind-the-scenes changes to their web mail client. (Just like when they started silently bouncing maillist mode messages from the Discourse forum... or blacklisted Nick's eMails.)

I still haven't explored to determine when and where Yahoo decides to strip linefeeds/carriage returns on outbound messages through the maillists. So I double them between paragraphs just to be safe. Sometimes they consolidate, sometimes they don't. 
author_association: CONTRIBUTOR
user: login: Nick-Hall
created_at: 2025-03-13T22:21:45Z
body: @emyoulation Please test that this works on Windows.
author_association: MEMBER
user: login: emyoulation
created_at: 2025-03-13T23:15:10Z
body: > @emyoulation Please test that this works on Windows.

OK but what tests do you want? <mark>Have you built a new AIOinstaller for me to test? I have not built from source on Windows.</mark>

I am resetting the PC now.  

It seems like a full test would be:
  1) install Gramps 4.28 and import example.gramps into BSDDB in %AppData% 
          (make a backup of the  %AppData%/gramps folder to desktop)
  2) upgrade to Gramps 5.2.4 and import example.gramps into SQLite in %AppData% (copy to backup appdata folder)         
  3) run AIO 6.0 installer candidate  
        see what happened to %AppData% and %LocalAppData%

<hr />

  4) uninstall all versions of gramps. Strip any `gramps` folder from %AppData% and %LocalAppData%
  5) install 5.2.4 and import example.gramps into SQLite in %LocalAppData% 
          (make a backup of the  %LocalAppData%/gramps folder to desktop)
  6) run AIO 6.0 installer candidate  
        see what happened to %AppData% and %LocalAppData%

<hr />

  7) uninstall 6.0,
  8) copy the descktop backups of AppData/gramps and LocalAppData/gramps to %AppData% and %LocalAppData% 
  9) run AIO 6.0 installer candidate  
        see what happened to %AppData% and %LocalAppData%
   
author_association: CONTRIBUTOR
user: login: emyoulation
created_at: 2025-03-14T00:48:05Z
body: OK. I've generated fresh sample  `AppData/Roaming/gramps`  and  `AppData/Local/gramps`  directories  with 4.2.8 and 5.2.4 and can test various 6.0 installer scenarios

(For all installs,`enchant` and `gtk-3.0` folders were created in `AppData/Local` )
author_association: CONTRIBUTOR
user: login: Nick-Hall
created_at: 2025-03-14T01:07:16Z
body: One option is to merge this and include it in 6.0.0-rc2, but do we want to test it further first?
author_association: MEMBER
user: login: emyoulation
created_at: 2025-03-14T01:21:37Z
body: Then I do not understand how I would test this further first. Isn't the `workflows/gramps-ci.yml` either part of the installer executable ? or maybe the build of the installer?  

The other 4 look like they should be easily added to an instance but when are those files called? Does it not need a full installer build to test?
author_association: CONTRIBUTOR
user: login: jralls
created_at: 2025-03-14T03:04:31Z
body: @emyoulation It's pure python. Just install the AIO to somewhere that isn't C:\Program Files\ and apply the patch.
author_association: MEMBER
user: login: emyoulation
created_at: 2025-03-14T03:19:28Z
body: I do not have a development system or full Python environment.  Just what the Gramps AIO installs. 

Doug had questions about what the MS windows exe included when we failed to make the AI chat addon functional or pip to install the prerequisites. His comments implied that the bundled environment was less than he expected. Or existed in a different configuration.

Since I have little experience with Python outside of the Python Eval and SuperTool gramplets,  maybe "it is pure python" doesn't communicate as much info to me as it does to you.

author_association: CONTRIBUTOR
user: login: dsblank
created_at: 2025-03-14T12:33:13Z
body: @emyoulation, ok this might be beyond you capabilities. No problem!

> I do not have a development system or full Python environment. Just what the Gramps AIO installs.

You only need to replace the Python code in this PR. But I don't think it is straightforward for you.
author_association: MEMBER
user: login: emyoulation
created_at: 2025-03-16T07:21:53Z
body: The Window AIO seems to be moving folders correctly and complaining appropriately when there is a conflict created by having gramps data folders in both local and roaming.

A question.  Are the Enchant dictionaries still supposed be installed in the windows %LocalAppData% even though the gramps folder has reverted to %AppData% (roaming) folder?  
author_association: CONTRIBUTOR