Skip to content

Instantly share code, notes, and snippets.

@m1st0
Last active July 27, 2025 11:46
Show Gist options
  • Save m1st0/269634f155fcc9a43dac2e9b79ba492e to your computer and use it in GitHub Desktop.
Save m1st0/269634f155fcc9a43dac2e9b79ba492e to your computer and use it in GitHub Desktop.
Fix /etc/apt/sources.list.d/ sources files with suite/release and key updates.

APT Sources Fixer

This Python script is designed to fix APT sources files located in /etc/apt/sources.list.d/. It updates the suite/release and key information, ensuring that your sources are correctly configured for package management.

🛠️ Features

  • Key Updates: Fetches and formats key information from a specified URL or file.
  • Suite Management: Prompts for changes to the suite if it is set to "oracular" or other values.
  • Backup Creation: Automatically creates a backup of the original sources file before making changes.
  • Diff Preview: Displays a diff of the changes before applying them.

📜 License

This project is licensed under the BSD License 2.0. See the LICENSE.txt file for details.

📧 Author

  • Author: Maulik Mistry with AI support.
  • Support: If you appreciate my work, consider supporting me through donations at PayPal.

📥 Installation

To use this script, ensure you have Python 3 and the required libraries installed. You can install the necessary libraries using pip:

pip install requests rich

Usage

Clone the repository:

git clone https://github.com/yourusername/apt-sources-fixer.git
cd apt-sources-fixer

Run the script:

python3 apt_fix_sources.py /etc/apt/sources.list.d/
#!/usr/bin/env python3
# Fix /etc/apt/sources.list.d/ sources files with suite/release and key updates.
# Copyright (c) 2025 Maulik Mistry
#
# This project is licensed under the BSD License. See the LICENSE.txt file for details.
#
# If you appreciate my work or help, consider supporting me through donations.
# You can donate via Venmo at @MaulikMistry or PayPal at https://www.paypal.com/paypalme/m1st0
import sys
import shutil
import re
import requests
import difflib
import argparse
from pathlib import Path
from rich.console import Console
from rich.markup import escape
console = Console()
def fetch_key(key_source):
try:
if key_source.startswith(('http://', 'https://')):
response = requests.get(key_source)
response.raise_for_status()
return response.text.splitlines()
else:
# Handle file path fetching
key_path = Path(key_source).expanduser().resolve()
if not key_path.is_file():
raise RuntimeError(f"The file at {key_path} does not exist or is not a valid file.")
with open(key_path, 'r') as f:
return f.read().splitlines() # Read file contents and split into lines
except Exception as e:
raise RuntimeError(f"Failed to fetch key from {key_source}: {e}")
def format_key(key_lines):
if not key_lines:
return []
formatted = []
# First line should always have "Signed-By:"
formatted.append(f"Signed-By: {key_lines[0].strip()}")
# Process the rest of the key lines
for line in key_lines[1:]:
stripped = line.strip()
if stripped == "":
# Only insert a dot if the line is completely empty after stripping
formatted.append(" .")
else:
# Otherwise, just add the line with the required indentation
formatted.append(f" {stripped}")
return formatted
def generate_new_content(original_lines, key_lines):
output_lines = []
skip_signed_by = False
for line in original_lines:
stripped = line.lstrip()
if stripped.startswith('Enabled: no'):
continue
if skip_signed_by:
if not line.startswith((' ', '\t')):
skip_signed_by = False
else:
continue
if re.match(r'^\s*Signed-By:', line):
skip_signed_by = True
continue
output_lines.append(line.rstrip('\n'))
insert_idx = None
for idx, line in enumerate(output_lines):
if line.strip().startswith('Components:'):
insert_idx = idx + 1
break
if insert_idx is None:
insert_idx = len(output_lines)
new_lines = output_lines[:insert_idx] + format_key(key_lines) + output_lines[insert_idx:]
return [line + '\n' for line in new_lines]
def show_diff(original_lines, new_lines, file_path):
diff = difflib.unified_diff(
original_lines,
new_lines,
fromfile=f"{file_path} (original)",
tofile=f"{file_path} (new)",
lineterm=""
)
for line in diff:
if line.startswith('+') and not line.startswith('+++'):
console.print(f"[green]{escape(line)}[/green]")
elif line.startswith('-') and not line.startswith('---'):
console.print(f"[red]{escape(line)}[/red]")
elif line.startswith('@@'):
console.print(f"[cyan]{escape(line)}[/cyan]")
else:
console.print(escape(line))
def backup_and_save(file_path, new_lines):
backup_path = file_path.with_suffix(file_path.suffix + '.bak')
shutil.copy2(file_path, backup_path)
console.print(f"[green]Backup created at {backup_path}[/green]")
with open(file_path, 'w') as f:
f.writelines(new_lines)
console.print(f"[green]Updated {file_path}[/green]\n")
def prompt_input(prompt, default=None):
if default:
prompt = f"{prompt} [{default}] "
else:
prompt = f"{prompt} "
value = input(prompt).strip()
return value if value else default
def process_file(file_path, retry_fetch=True):
with open(file_path, 'r') as f:
original_lines = f.readlines()
# Show URI to user
uri = extract_uri(original_lines)
if uri:
console.print(f"\n[bold]Found URI:[/bold] {uri}")
# Check Suites/Suite
suite_key, suite_value = extract_suite(original_lines)
if suite_value:
console.print(f"[bold]Found {suite_key}:[/bold] {suite_value}")
if suite_value.lower() == "oracular":
fix = prompt_input(f"Change {suite_key} from 'oracular' to 'plucky'? [Y/n]", "Y").lower()
if fix == 'y':
# Replace the suite line
for idx, line in enumerate(original_lines):
if line.strip().startswith((suite_key + ":")):
original_lines[idx] = f"{suite_key}: plucky\n"
break
else:
fix = prompt_input(f"Suite '{suite_value}' found. Change to 'plucky'? [y/N]", "N").lower()
if fix == 'y':
for idx, line in enumerate(original_lines):
if line.strip().startswith((suite_key + ":")):
original_lines[idx] = f"{suite_key}: plucky\n"
break
while True:
key_source = input(f"\nEnter key URL or file path for [bold]{file_path.name}[/bold] (ENTER to skip): ").strip()
if not key_source:
console.print("[yellow]Skipping file.[/yellow]\n")
return 'skipped'
try:
key_lines = fetch_key(key_source)
break
except Exception as e:
console.print(f"[red]{e}[/red]")
if not retry_fetch or prompt_input("Retry fetching key? [Y/n]", "Y").lower() != 'y':
console.print("[yellow]Skipping file.[/yellow]\n")
return 'skipped'
new_lines = generate_new_content(original_lines, key_lines)
console.print("\n[cyan]--- Diff Preview ---[/cyan]")
show_diff(original_lines, new_lines, file_path)
console.print("[cyan]--------------------[/cyan]\n")
confirm = prompt_input("Apply changes? [Y/n]", "Y").lower()
if confirm == 'y':
backup_and_save(file_path, new_lines)
return 'modified'
else:
console.print("[yellow]Changes discarded.[/yellow]\n")
return 'skipped'
def extract_uri(lines):
for line in lines:
if line.strip().startswith("URIs:"):
return line.strip().split(":", 1)[1].strip()
return None
def extract_suite(lines):
for line in lines:
if line.strip().startswith(("Suite:", "Suites:")):
key, value = line.strip().split(":", 1)
return key, value.strip()
return None, None
def main():
parser = argparse.ArgumentParser(
description="Fix .sources files by removing old Signed-By/Enabled lines and inserting new keys."
)
parser.add_argument(
"directory",
type=Path,
help="Directory containing .sources files (e.g., /etc/apt/sources.list.d/)"
)
args = parser.parse_args()
sources_dir = args.directory.resolve()
if not sources_dir.is_dir():
console.print(f"[red]{sources_dir} is not a valid directory![/red]")
sys.exit(1)
sources_files = sorted(sources_dir.glob('*.sources'))
if not sources_files:
console.print("[yellow]No .sources files found.[/yellow]")
sys.exit(0)
total_files = len(sources_files)
modified = 0
skipped = 0
console.print(f"\nFound {total_files} .sources file(s) in [bold]{sources_dir}[/bold]\n")
for idx, file_path in enumerate(sources_files, start=1):
console.rule(f"File {idx}/{total_files}: {file_path.name}")
choice = prompt_input("Process this file? [Y/n/q to quit]", "Y").lower()
if choice == 'y':
result = process_file(file_path)
if result == 'modified':
modified += 1
else:
skipped += 1
elif choice == 'q':
console.print("[bold]Exiting early by user request.[/bold]")
break
else:
console.print("[yellow]Skipping.[/yellow]\n")
skipped += 1
# 🎉 Final Summary
console.rule("[bold cyan]Summary[/bold cyan]")
console.print(f"[green]Modified:[/green] {modified}")
console.print(f"[yellow]Skipped:[/yellow] {skipped}")
console.print(f"[blue]Total processed:[/blue] {modified + skipped}\n")
if __name__ == "__main__":
main()
BSD 2-Clause License
Copyright (c) 2025, Maulik Mistry
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING INANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment