I wanted to make a script to check if updated flatpak app permissions have changed before updating them. I decided to use ChatGPTs newest model, 01-preview for this task and holy crap am I impressed. I had to give it 14 commands to get to the final product and it seems to work great. That number could have been reduced quite a bit had I done things a bit differently.
I still had to find the problems, look up reference docs to correct it, and even debug in an IDE a little. But just telling it where the problems were, it got there in the end, and its user interaction output is way better than what I was planning on doing.
Sections:
<?php
// -- This script checks for Flatpak updates and reports permission changes. --
// Function to parse permissions from Flatpak metadata
function parsePermissions($content) {
$permissions = [];
$lines = is_array($content) ? $content : explode("\n", $content);
$currentSection = '';
$skipSections = ['Application']; // Sections to skip
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) {
continue;
}
// Check for section headers
if (preg_match('/^\[(.*)\]$/', $line, $matches)) {
$currentSection = $matches[1];
// Skip the [Application] section
if (in_array($currentSection, $skipSections)) {
$currentSection = '';
continue;
}
$permissions[$currentSection] = [];
} elseif ($currentSection !== '') {
// Only process lines within non-skipped sections
$parts = explode('=', $line, 2);
if (count($parts) == 2) {
$key = $parts[0];
$values = explode(';', trim($parts[1], ';'));
$permissions[$currentSection][$key] = $values;
} else {
// Handle keys without '=' (e.g., single permissions)
$permissions[$currentSection][$line] = [];
}
}
}
return $permissions;
}
// Function to compare permissions
function comparePermissions($current, $new) {
$differences = [];
// Get all sections
$sections = array_unique(array_merge(array_keys($current), array_keys($new)));
foreach ($sections as $section) {
$currentSection = isset($current[$section]) ? $current[$section] : [];
$newSection = isset($new[$section]) ? $new[$section] : [];
// Get all keys in this section
$keys = array_unique(array_merge(array_keys($currentSection), array_keys($newSection)));
foreach ($keys as $key) {
$currentValues = isset($currentSection[$key]) ? $currentSection[$key] : [];
$newValues = isset($newSection[$key]) ? $newSection[$key] : [];
// Compare values
$added = array_diff($newValues, $currentValues);
$removed = array_diff($currentValues, $newValues);
if (!empty($added) || !empty($removed)) {
$differences[$section][$key] = [
'added' => $added,
'removed' => $removed,
];
}
}
}
return $differences;
}
// Function to output a line with appId (if given), colored red (IsError=true) or green (IsError=false)
function outputColor($appId, $str, $isError=true, $NoNewline=false)
{
// Determine if coloring should be used
static $hasColors=null;
if(!isset($hasColors)) {
$hasColors=stream_isatty(STDOUT) && (!trim(`command -v tput`) || intval(`tput colors`)>=16);
}
echo
(!$hasColors ? '' : ($isError ? "\e[31m" : "\e[32m")).
($appId ? "$appId: " : '').
$str.
(!$hasColors ? '' : "\e[0m").
($NoNewline ? '' : "\n");
}
// Function to display permission differences
function displayDifferences($appId, $differences) {
outputColor($appId, 'Permission changes:');
foreach ($differences as $section => $keys) {
outputColor(null, " [$section]");
foreach ($keys as $key => $changes) {
if (!empty($changes['added'])) {
outputColor(null, " + $key = " . implode(';', $changes['added']));
}
if (!empty($changes['removed'])) {
outputColor(null, " - $key = " . implode(';', $changes['removed']));
}
}
}
echo "\n";
}
// Function to ask the user a question
function askQuestion($Question) {
outputColor('', "$Question (y/N): ", true, true);
$handle = fopen('php://stdin', 'r');
$line = fgets($handle);
$answer = trim(strtolower($line));
fclose($handle);
return ($answer == 'y' || $answer == 'yes');
}
// Get architecture (moved above the loop)
$archCommand = 'flatpak --default-arch';
exec($archCommand, $archOutput);
$arch = trim(implode('', $archOutput));
// Step 1: Get the list of installed Flatpaks
$installedCommand = 'flatpak list -a --columns=application,branch,origin,options';
exec($installedCommand, $installedOutput);
// Remove header line if present
if (!empty($installedOutput) && strpos($installedOutput[0], 'Application ID') !== false) {
array_shift($installedOutput);
}
// Build an associative array of installed applications
$installedApps = [];
foreach ($installedOutput as $line) {
// The output is tab-delimited
$columns = explode("\t", trim($line));
if (count($columns) >= 4) {
$appId = $columns[0];
$branch = $columns[1];
$origin = $columns[2];
$options = $columns[3];
$installedApps[$appId] = [
'appId' => $appId,
'branch' => $branch,
'origin' => $origin,
'options' => $options,
];
}
}
// Get the list of updates
exec('echo n | flatpak update', $updatesOutput);
foreach ($updatesOutput as $Line) {
if (preg_match('/^ *\d+\.\s+([\w\.-]+)/', $Line, $matches)) {
$updatesAvailable[]=$matches[1];
}
}
// Let the user confirm the updates
echo str_repeat('-', 80)."\n";
echo implode("\n", $updatesOutput)."\n";
echo str_repeat('-', 80)."\n";
if (empty($updatesAvailable)) {
outputColor('', 'No updates available for installed Flatpaks.');
exit(0);
}
if (!askQuestion('Found '.count($updatesAvailable).' updates. Continue?')) {
echo "Updates canceled.\n";
exit(0);
}
$permissionChanges = [];
foreach ($updatesAvailable as $appId) {
if (!isset($installedApps[$appId])) {
outputColor($appId, 'Installed app not found. Skipping.');
continue;
}
$app = $installedApps[$appId];
$branch = $app['branch'];
$origin = $app['origin'];
$options = $app['options'];
// Determine if it's an app or runtime
$isRuntime = strpos($options, 'runtime') !== false;
// Paths to the metadata files
if ($isRuntime) {
$metadataPath = "/var/lib/flatpak/runtime/$appId/$arch/$branch/active/metadata";
} else {
$metadataPath = "/var/lib/flatpak/app/$appId/$arch/$branch/active/metadata";
}
// Check if the metadata file exists
if (!file_exists($metadataPath)) {
outputColor($appId, 'Metadata file not found. Skipping.');
continue;
}
// Read current permissions from the metadata file
$metadataContent = file_get_contents($metadataPath);
$currentPermissions = parsePermissions($metadataContent);
// Get new metadata from remote
$ref = $appId . '/' . $arch . '/' . $branch;
$remoteInfoCommand = 'flatpak remote-info --show-metadata ' . escapeshellarg($origin) . ' ' . escapeshellarg($ref);
// Clear $remoteOutput before exec()
$remoteOutput = [];
exec($remoteInfoCommand, $remoteOutput);
if (empty($remoteOutput)) {
outputColor($appId, 'Failed to retrieve remote metadata. Skipping.');
continue;
}
// Parse new permissions from the remote metadata
$newPermissions = parsePermissions($remoteOutput);
// Compare permissions
$differences = comparePermissions($currentPermissions, $newPermissions);
if (!empty($differences)) {
$permissionChanges[$appId] = $differences;
displayDifferences($appId, $differences);
} else {
outputColor($appId, 'No permission changes found.', false);
}
}
// If there are no permission changes, inform the user
if (empty($permissionChanges)) {
echo "No permission changes detected in the available updates.\n";
}
// Ask user if they want to proceed
if (askQuestion('Do you want to proceed with the updates?')) {
// Proceed with updates
echo "Updating Flatpaks...\n";
passthru('flatpak update -y');
} else {
echo "Updates canceled.\n";
}
?>
For demonstration purposes, I have left all of my original typos uncorrected below.
<?php
/**
* This script checks for Flatpak updates and reports permission changes.
*/
// Function to parse permissions from Flatpak metadata
function parsePermissions($content) {
$permissions = [];
$lines = is_array($content) ? $content : explode("\n", $content);
$currentSection = '';
$skipSections = ['Application']; // Sections to skip
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) {
continue;
}
// Check for section headers
if (preg_match('/^\[(.*)\]$/', $line, $matches)) {
$currentSection = $matches[1];
// Skip the [Application] section
if (in_array($currentSection, $skipSections)) {
$currentSection = '';
continue;
}
$permissions[$currentSection] = [];
} elseif ($currentSection !== '') {
// Only process lines within non-skipped sections
$parts = explode('=', $line, 2);
if (count($parts) == 2) {
$key = $parts[0];
$values = explode(';', trim($parts[1], ';'));
$permissions[$currentSection][$key] = $values;
} else {
// Handle keys without '=' (e.g., single permissions)
$permissions[$currentSection][$line] = [];
}
}
}
return $permissions;
}
// Function to compare permissions
function comparePermissions($current, $new) {
$differences = [];
// Get all sections
$sections = array_unique(array_merge(array_keys($current), array_keys($new)));
foreach ($sections as $section) {
$currentSection = isset($current[$section]) ? $current[$section] : [];
$newSection = isset($new[$section]) ? $new[$section] : [];
// Get all keys in this section
$keys = array_unique(array_merge(array_keys($currentSection), array_keys($newSection)));
foreach ($keys as $key) {
$currentValues = isset($currentSection[$key]) ? $currentSection[$key] : [];
$newValues = isset($newSection[$key]) ? $newSection[$key] : [];
// Compare values
$added = array_diff($newValues, $currentValues);
$removed = array_diff($currentValues, $newValues);
if (!empty($added) || !empty($removed)) {
$differences[$section][$key] = [
'added' => $added,
'removed' => $removed,
];
}
}
}
return $differences;
}
// Function to display permission differences
function displayDifferences($appId, $differences) {
echo "Permission changes for $appId:\n";
foreach ($differences as $section => $keys) {
echo " [$section]\n";
foreach ($keys as $key => $changes) {
if (!empty($changes['added'])) {
echo " + $key = " . implode(';', $changes['added']) . "\n";
}
if (!empty($changes['removed'])) {
echo " - $key = " . implode(';', $changes['removed']) . "\n";
}
}
}
echo "\n";
}
// Get architecture (moved above the loop)
$archCommand = 'flatpak --default-arch';
exec($archCommand, $archOutput);
$arch = trim(implode('', $archOutput));
// Step 1: Get the list of installed Flatpaks
$installedCommand = 'flatpak list --columns=application,branch,origin,options';
exec($installedCommand, $installedOutput);
// Remove header line if present
if (!empty($installedOutput) && strpos($installedOutput[0], 'Application ID') !== false) {
array_shift($installedOutput);
}
// Build an associative array of installed applications
$installedApps = [];
foreach ($installedOutput as $line) {
// The output is tab-delimited
$columns = explode("\t", trim($line));
if (count($columns) >= 4) {
$appId = $columns[0];
$branch = $columns[1];
$origin = $columns[2];
$options = $columns[3];
$installedApps[$appId] = [
'appId' => $appId,
'branch' => $branch,
'origin' => $origin,
'options' => $options,
];
}
}
// Get the list of available updates
$updatesCommand = 'flatpak remote-ls --updates --columns=application';
exec($updatesCommand, $updatesOutput);
// Remove header line if present
if (!empty($updatesOutput) && strpos($updatesOutput[0], 'Application ID') !== false) {
array_shift($updatesOutput);
}
// Build a list of applications that have updates
$updatesAvailable = array_map('trim', $updatesOutput);
if (empty($updatesAvailable)) {
echo "No updates available for installed Flatpaks.\n";
exit(0);
}
$permissionChanges = [];
foreach ($updatesAvailable as $appId) {
if (!isset($installedApps[$appId])) {
// The updated app is not in the installed apps list, skip it
continue;
}
$app = $installedApps[$appId];
$branch = $app['branch'];
$origin = $app['origin'];
$options = $app['options'];
// Determine if it's an app or runtime
$isRuntime = strpos($options, 'runtime') !== false;
// Paths to the metadata files
if ($isRuntime) {
$metadataPath = "/var/lib/flatpak/runtime/$appId/$arch/$branch/active/metadata";
} else {
$metadataPath = "/var/lib/flatpak/app/$appId/$arch/$branch/active/metadata";
}
// Check if the metadata file exists
if (!file_exists($metadataPath)) {
echo "Metadata file not found for $appId. Skipping.\n";
continue;
}
// Read current permissions from the metadata file
$metadataContent = file_get_contents($metadataPath);
$currentPermissions = parsePermissions($metadataContent);
// Get new metadata from remote
$ref = $appId . '/' . $arch . '/' . $branch;
$remoteInfoCommand = 'flatpak remote-info --show-metadata ' . escapeshellarg($origin) . ' ' . escapeshellarg($ref);
// Clear $remoteOutput before exec()
$remoteOutput = [];
exec($remoteInfoCommand, $remoteOutput);
if (empty($remoteOutput)) {
echo "Failed to retrieve remote metadata for $appId. Skipping.\n";
continue;
}
// Parse new permissions from the remote metadata
$newPermissions = parsePermissions($remoteOutput);
// Compare permissions
$differences = comparePermissions($currentPermissions, $newPermissions);
if (!empty($differences)) {
$permissionChanges[$appId] = $differences;
displayDifferences($appId, $differences);
}
}
// If there are no permission changes, inform the user
if (empty($permissionChanges)) {
echo "No permission changes detected in the available updates.\n";
}
// Ask user if they want to proceed
echo "Do you want to proceed with the updates? (y/N): ";
$handle = fopen("php://stdin", "r");
$line = fgets($handle);
$answer = trim(strtolower($line));
if ($answer == 'y' || $answer == 'yes') {
// Proceed with updates
echo "Updating Flatpaks...\n";
passthru('flatpak update -y');
} else {
echo "Updates canceled.\n";
}
fclose($handle);
?>
io.github.ungoogled_software.ungoogled_chromium: Permission changes:
[Context]
+ filesystems = xdg-run/pipewire-0;~/.local/share/icons:create;xdg-download;xdg-desktop;~/.local/share/applications:create
- filesystems = home
[Environment]
- LD_LIBRARY_PATH = /app/chromium/nonfree-codecs/lib
[Extension io.github.ungoogled_software.ungoogled_chromium.Codecs]
- directory = chromium/nonfree-codecs
- autodelete = true
- add-ld-path = lib
[Build]
- built-extensions = io.github.ungoogled_software.ungoogled_chromium.Codecs